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 }