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 }