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-2022 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
15module dmd.common.file;
16
17import core.stdc.errno : errno;
18import core.stdc.stdio : fprintf, remove, rename, stderr;
19import core.stdc.stdlib : exit;
20import core.stdc.string : strerror;
21import core.sys.windows.winbase;
22import core.sys.windows.winnt;
23import core.sys.posix.fcntl;
24import core.sys.posix.unistd;
25
26import dmd.common.string;
27
28nothrow:
29
30/**
31Encapsulated management of a memory-mapped file.
32
33Params:
34Datum = the mapped data type: Use a POD of size 1 for read/write mapping
35and a `const` version thereof for read-only mapping. Other primitive types
36should work, but have not been yet tested.
37*/
38struct 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        auto totalNameLength = filename.strlen() + 1;
148        name = cast(char*) memcpy(malloc(totalNameLength), filename, totalNameLength);
149        name || assert(0, "FileMapping: Out of memory.");
150    }
151
152    /**
153    Common code factored opportunistically. Windows only. Assumes `handle` is
154    already pointing to an opened file. Initializes the `fileMappingObject`
155    and `data` members.
156
157    Params:
158    filename = the file to be mapped
159    size = the size of the file in bytes
160    */
161    version(Windows) private void createMapping(const char* filename, ulong size)
162    {
163        assert(size <= size_t.max || size == ulong.max);
164        assert(handle != invalidHandle);
165        assert(data is null);
166        assert(fileMappingObject == invalidHandle);
167
168        if (size == 0 || size == ulong.max)
169            return;
170
171        static if (is(Datum == const))
172        {
173            enum fileMappingFlags = PAGE_READONLY;
174            enum mapViewFlags = FILE_MAP_READ;
175        }
176        else
177        {
178            enum fileMappingFlags = PAGE_READWRITE;
179            enum mapViewFlags = FILE_MAP_WRITE;
180        }
181
182        fileMappingObject = CreateFileMappingW(handle, null, fileMappingFlags, 0, 0, null);
183        if (!fileMappingObject)
184        {
185            fprintf(stderr, "CreateFileMappingW(%p) failed for %llu bytes of \"%s\": %d\n",
186                handle, size, filename, GetLastError());
187            fileMappingObject = invalidHandle;  // by convention always use invalidHandle, not null
188            exit(1);
189        }
190        auto p = MapViewOfFile(fileMappingObject, mapViewFlags, 0, 0, 0);
191        if (!p)
192        {
193            fprintf(stderr, "MapViewOfFile() failed for \"%s\": %d\n", filename, GetLastError());
194            exit(1);
195        }
196        data = cast(Datum[]) p[0 .. cast(size_t) size];
197    }
198
199    // Not copyable or assignable (for now).
200    @disable this(const FileMapping!Datum rhs);
201    @disable void opAssign(const ref FileMapping!Datum rhs);
202
203    /**
204    Frees resources associated with this mapping. However, it does not deallocate the name.
205    */
206    ~this() pure nothrow
207    {
208        if (!active)
209            return;
210        fakePure({
211            version (Posix)
212            {
213                import core.sys.posix.sys.mman : munmap;
214                import core.sys.posix.unistd : close;
215
216                // Cannot call fprintf from inside a destructor, so exiting silently.
217
218                if (data.ptr && munmap(cast(void*) data.ptr, data.length) != 0)
219                {
220                    exit(1);
221                }
222                data = null;
223                if (handle != invalidHandle && close(handle) != 0)
224                {
225                    exit(1);
226                }
227                handle = invalidHandle;
228            }
229            else version(Windows)
230            {
231                if (data.ptr !is null && UnmapViewOfFile(cast(void*) data.ptr) == 0)
232                {
233                    exit(1);
234                }
235                data = null;
236                if (fileMappingObject != invalidHandle && CloseHandle(fileMappingObject) == 0)
237                {
238                    exit(1);
239                }
240                fileMappingObject = invalidHandle;
241                if (handle != invalidHandle && CloseHandle(handle) == 0)
242                {
243                    exit(1);
244                }
245                handle = invalidHandle;
246            }
247            else static assert(0);
248        });
249    }
250
251    /**
252    Returns the zero-terminated file name associated with the mapping. Can NOT
253    be saved beyond the lifetime of `this`.
254    */
255    private const(char)* filename() const pure @nogc @safe nothrow { return name; }
256
257    /**
258    Frees resources associated with this mapping. However, it does not deallocate the name.
259    Reinitializes `this` as a fresh object that can be reused.
260    */
261    void close()
262    {
263        __dtor();
264        handle = invalidHandle;
265        version(Windows) fileMappingObject = invalidHandle;
266        data = null;
267        name = null;
268    }
269
270    /**
271    Deletes the underlying file and frees all resources associated.
272    Reinitializes `this` as a fresh object that can be reused.
273
274    This function does not abort if the file cannot be deleted, but does print
275    a message on `stderr` and returns `false` to the caller. The underlying
276    rationale is to give the caller the option to continue execution if
277    deleting the file is not important.
278
279    Returns: `true` iff the file was successfully deleted. If the file was not
280    deleted, prints a message to `stderr` and returns `false`.
281    */
282    static if (!is(Datum == const))
283    bool discard()
284    {
285        // Truncate file to zero so unflushed buffers are not flushed unnecessarily.
286        resize(0);
287        auto deleteme = name;
288        close();
289        // In-memory resource freed, now get rid of the underlying temp file.
290        version(Posix)
291        {
292            import core.sys.posix.unistd : unlink;
293            if (unlink(deleteme) != 0)
294            {
295                fprintf(stderr, "unlink(\"%s\") failed: %s\n", filename, strerror(errno));
296                return false;
297            }
298        }
299        else version(Windows)
300        {
301            import core.sys.windows.winbase;
302            if (deleteme.asDString.extendedPathThen!(p => DeleteFileW(p.ptr)) == 0)
303            {
304                fprintf(stderr, "DeleteFileW error %d\n", GetLastError());
305                return false;
306            }
307        }
308        else static assert(0);
309        return true;
310    }
311
312    /**
313    Queries whether `this` is currently associated with a file.
314
315    Returns: `true` iff there is an active mapping.
316    */
317    bool active() const pure @nogc nothrow
318    {
319        return handle !is invalidHandle;
320    }
321
322    /**
323    Queries the length of the file associated with this mapping.  If not
324    active, returns 0.
325
326    Returns: the length of the file, or 0 if no file associated.
327    */
328    size_t length() const pure @nogc @safe nothrow { return data.length; }
329
330    /**
331    Get a slice to the contents of the entire file.
332
333    Returns: the contents of the file. If not active, returns the `null` slice.
334    */
335    auto opSlice() pure @nogc @safe nothrow { return data; }
336
337    /**
338    Resizes the file and mapping to the specified `size`.
339
340    Params:
341    size = new length requested
342    */
343    static if (!is(Datum == const))
344    void resize(size_t size) pure
345    {
346        assert(handle != invalidHandle);
347        fakePure({
348            version(Posix)
349            {
350                import core.sys.posix.unistd : ftruncate;
351                import core.sys.posix.sys.mman;
352
353                if (data.length)
354                {
355                    assert(data.ptr, "Corrupt memory mapping");
356                    // assert(0) here because it would indicate an internal error
357                    munmap(cast(void*) data.ptr, data.length) == 0 || assert(0);
358                    data = null;
359                }
360                if (ftruncate(handle, size) != 0)
361                {
362                    fprintf(stderr, "ftruncate() failed for \"%s\": %s\n", filename, strerror(errno));
363                    exit(1);
364                }
365                if (size > 0)
366                {
367                    auto p = mmap(null, size, PROT_WRITE, MAP_SHARED, handle, 0);
368                    if (cast(ssize_t) p == -1)
369                    {
370                        fprintf(stderr, "mmap() failed for \"%s\": %s\n", filename, strerror(errno));
371                        exit(1);
372                    }
373                    data = cast(Datum[]) p[0 .. size];
374                }
375            }
376            else version(Windows)
377            {
378                // Per documentation, must unmap first.
379                if (data.length > 0 && UnmapViewOfFile(cast(void*) data.ptr) == 0)
380                {
381                    fprintf(stderr, "UnmapViewOfFile(%p) failed for memory mapping of \"%s\": %d\n",
382                        data.ptr, filename, GetLastError());
383                    exit(1);
384                }
385                data = null;
386                if (fileMappingObject != invalidHandle && CloseHandle(fileMappingObject) == 0)
387                {
388                    fprintf(stderr, "CloseHandle() failed for memory mapping of \"%s\": %d\n", filename, GetLastError());
389                    exit(1);
390                }
391                fileMappingObject = invalidHandle;
392                LARGE_INTEGER biggie;
393                biggie.QuadPart = size;
394                if (SetFilePointerEx(handle, biggie, null, FILE_BEGIN) == 0 || SetEndOfFile(handle) == 0)
395                {
396                    fprintf(stderr, "SetFilePointer() failed for \"%s\": %d\n", filename, GetLastError());
397                    exit(1);
398                }
399                createMapping(name, size);
400            }
401            else static assert(0);
402        });
403    }
404
405    /**
406    Unconditionally and destructively moves the underlying file to `filename`.
407    If the operation succeeds, returns true. Upon failure, prints a message to
408    `stderr` and returns `false`. In all cases it closes the underlying file.
409
410    Params: filename = zero-terminated name of the file to move to.
411
412    Returns: `true` iff the operation was successful.
413    */
414    bool moveToFile(const char* filename)
415    {
416        assert(name !is null);
417
418        // Fetch the name and then set it to `null` so it doesn't get deallocated
419        auto oldname = name;
420        import core.stdc.stdlib;
421        scope(exit) free(cast(void*) oldname);
422        name = null;
423        close();
424
425        // Rename the underlying file to the target, no copy necessary.
426        version(Posix)
427        {
428            if (.rename(oldname, filename) != 0)
429            {
430                fprintf(stderr, "rename(\"%s\", \"%s\") failed: %s\n", oldname, filename, strerror(errno));
431                return false;
432            }
433        }
434        else version(Windows)
435        {
436            import core.sys.windows.winbase;
437            auto r = oldname.asDString.extendedPathThen!(
438                p1 => filename.asDString.extendedPathThen!(p2 => MoveFileExW(p1.ptr, p2.ptr, MOVEFILE_REPLACE_EXISTING))
439            );
440            if (r == 0)
441            {
442                fprintf(stderr, "MoveFileExW(\"%s\", \"%s\") failed: %d\n", oldname, filename, GetLastError());
443                return false;
444            }
445        }
446        else static assert(0);
447        return true;
448    }
449}
450
451/// Write a file, returning `true` on success.
452extern(D) static bool writeFile(const(char)* name, const void[] data) nothrow
453{
454    version (Posix)
455    {
456        int fd = open(name, O_CREAT | O_WRONLY | O_TRUNC, (6 << 6) | (4 << 3) | 4);
457        if (fd == -1)
458            goto err;
459        if (.write(fd, data.ptr, data.length) != data.length)
460            goto err2;
461        if (close(fd) == -1)
462            goto err;
463        return true;
464    err2:
465        close(fd);
466        .remove(name);
467    err:
468        return false;
469    }
470    else version (Windows)
471    {
472        DWORD numwritten; // here because of the gotos
473        const nameStr = name.asDString;
474        // work around Windows file path length limitation
475        // (see documentation for extendedPathThen).
476        HANDLE h = nameStr.extendedPathThen!
477            (p => CreateFileW(p.ptr,
478                                GENERIC_WRITE,
479                                0,
480                                null,
481                                CREATE_ALWAYS,
482                                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
483                                null));
484        if (h == INVALID_HANDLE_VALUE)
485            goto err;
486
487        if (WriteFile(h, data.ptr, cast(DWORD)data.length, &numwritten, null) != TRUE)
488            goto err2;
489        if (numwritten != data.length)
490            goto err2;
491        if (!CloseHandle(h))
492            goto err;
493        return true;
494    err2:
495        CloseHandle(h);
496        nameStr.extendedPathThen!(p => DeleteFileW(p.ptr));
497    err:
498        return false;
499    }
500    else
501    {
502        static assert(0);
503    }
504}
505
506/// Touch a file to current date
507bool touchFile(const char* namez)
508{
509    version (Windows)
510    {
511        FILETIME ft = void;
512        SYSTEMTIME st = void;
513        GetSystemTime(&st);
514        SystemTimeToFileTime(&st, &ft);
515
516        import core.stdc.string : strlen;
517
518        // get handle to file
519        HANDLE h = namez[0 .. namez.strlen()].extendedPathThen!(p => CreateFile(p.ptr,
520            FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE,
521            null, OPEN_EXISTING,
522            FILE_ATTRIBUTE_NORMAL, null));
523        if (h == INVALID_HANDLE_VALUE)
524            return false;
525
526        const f = SetFileTime(h, null, null, &ft); // set last write time
527
528        if (!CloseHandle(h))
529            return false;
530
531        return f != 0;
532    }
533    else version (Posix)
534    {
535        import core.sys.posix.utime;
536        return utime(namez, null) == 0;
537    }
538    else
539        static assert(0);
540}
541
542// Feel free to make these public if used elsewhere.
543/**
544Size of a file in bytes.
545Params: fd = file handle
546Returns: file size in bytes, or `ulong.max` on any error.
547*/
548version (Posix)
549private ulong fileSize(int fd)
550{
551    import core.sys.posix.sys.stat;
552    stat_t buf;
553    if (fstat(fd, &buf) == 0)
554        return buf.st_size;
555    return ulong.max;
556}
557
558/// Ditto
559version (Windows)
560private ulong fileSize(HANDLE fd)
561{
562    ulong result;
563    if (GetFileSizeEx(fd, cast(LARGE_INTEGER*) &result) == 0)
564        return result;
565    return ulong.max;
566}
567
568/**
569Runs a non-pure function or delegate as pure code. Use with caution.
570
571Params:
572fun = the delegate to run, usually inlined: `fakePure({ ... });`
573
574Returns: whatever `fun` returns.
575*/
576private auto ref fakePure(F)(scope F fun) pure
577{
578    mixin("alias PureFun = " ~ F.stringof ~ " pure;");
579    return (cast(PureFun) fun)();
580}
581