1 // Written in the D programming language. 2 3 /** 4 * Read and write memory mapped files. 5 * Copyright: Copyright The D Language Foundation 2004 - 2009. 6 * License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 7 * Authors: $(HTTP digitalmars.com, Walter Bright), 8 * Matthew Wilson 9 * Source: $(PHOBOSSRC std/mmfile.d) 10 * 11 * $(SCRIPT inhibitQuickIndex = 1;) 12 */ 13 /* Copyright The D Language Foundation 2004 - 2009. 14 * Distributed under the Boost Software License, Version 1.0. 15 * (See accompanying file LICENSE_1_0.txt or copy at 16 * http://www.boost.org/LICENSE_1_0.txt) 17 */ 18 module std.mmfile; 19 20 version (WebAssembly) {} else: 21 22 import core.stdc.errno; 23 import core.stdc.stdio; 24 import core.stdc.stdlib; 25 import std.conv, std.exception, std.stdio; 26 import std.file; 27 import std.path; 28 import std.string; 29 30 import std.internal.cstring; 31 32 //debug = MMFILE; 33 34 version (Windows) 35 { 36 import core.sys.windows.winbase; 37 import core.sys.windows.winnt; 38 import std.utf; 39 import std.windows.syserror; 40 } 41 else version (Posix) 42 { 43 import core.sys.posix.fcntl; 44 import core.sys.posix.sys.mman; 45 import core.sys.posix.sys.stat; 46 import core.sys.posix.unistd; 47 } 48 else 49 { 50 static assert(0); 51 } 52 53 /** 54 * MmFile objects control the memory mapped file resource. 55 */ 56 class MmFile 57 { 58 /** 59 * The mode the memory mapped file is opened with. 60 */ 61 enum Mode 62 { 63 read, /// Read existing file 64 readWriteNew, /// Delete existing file, write new file 65 readWrite, /// Read/Write existing file, create if not existing 66 readCopyOnWrite, /// Read/Write existing file, copy on write 67 } 68 69 /** 70 * Open memory mapped file filename for reading. 71 * File is closed when the object instance is deleted. 72 * Throws: 73 * - On POSIX, $(REF ErrnoException, std, exception). 74 * - On Windows, $(REF WindowsException, std, windows, syserror). 75 */ 76 this(string filename) scope 77 { 78 this(filename, Mode.read, 0, null); 79 } 80 81 version (linux) this(File file, Mode mode = Mode.read, ulong size = 0, 82 void* address = null, size_t window = 0) scope 83 { 84 // Save a copy of the File to make sure the fd stays open. 85 this.file = file; 86 this(file.fileno, mode, size, address, window); 87 } 88 89 version (linux) private this(int fildes, Mode mode, ulong size, 90 void* address, size_t window) scope 91 { 92 int oflag; 93 int fmode; 94 95 final switch (mode) 96 { 97 case Mode.read: 98 flags = MAP_SHARED; 99 prot = PROT_READ; 100 oflag = O_RDONLY; 101 fmode = 0; 102 break; 103 104 case Mode.readWriteNew: 105 assert(size != 0); 106 flags = MAP_SHARED; 107 prot = PROT_READ | PROT_WRITE; 108 oflag = O_CREAT | O_RDWR | O_TRUNC; 109 fmode = octal!660; 110 break; 111 112 case Mode.readWrite: 113 flags = MAP_SHARED; 114 prot = PROT_READ | PROT_WRITE; 115 oflag = O_CREAT | O_RDWR; 116 fmode = octal!660; 117 break; 118 119 case Mode.readCopyOnWrite: 120 flags = MAP_PRIVATE; 121 prot = PROT_READ | PROT_WRITE; 122 oflag = O_RDWR; 123 fmode = 0; 124 break; 125 } 126 127 fd = fildes; 128 129 // Adjust size 130 stat_t statbuf = void; 131 errnoEnforce(fstat(fd, &statbuf) == 0); 132 if (prot & PROT_WRITE && size > statbuf.st_size) 133 { 134 // Need to make the file size bytes big 135 lseek(fd, cast(off_t)(size - 1), SEEK_SET); 136 char c = 0; 137 core.sys.posix.unistd.write(fd, &c, 1); 138 } 139 else if (prot & PROT_READ && size == 0) 140 size = statbuf.st_size; 141 this.size = size; 142 143 // Map the file into memory! 144 size_t initial_map = (window && 2*window<size) 145 ? 2*window : cast(size_t) size; 146 auto p = mmap(address, initial_map, prot, flags, fd, 0); 147 if (p == MAP_FAILED) 148 { 149 errnoEnforce(false, "Could not map file into memory"); 150 } 151 data = p[0 .. initial_map]; 152 } 153 154 /** 155 * Open memory mapped file filename in mode. 156 * File is closed when the object instance is deleted. 157 * Params: 158 * filename = name of the file. 159 * If null, an anonymous file mapping is created. 160 * mode = access mode defined above. 161 * size = the size of the file. If 0, it is taken to be the 162 * size of the existing file. 163 * address = the preferred address to map the file to, 164 * although the system is not required to honor it. 165 * If null, the system selects the most convenient address. 166 * window = preferred block size of the amount of data to map at one time 167 * with 0 meaning map the entire file. The window size must be a 168 * multiple of the memory allocation page size. 169 * Throws: 170 * - On POSIX, $(REF ErrnoException, std, exception). 171 * - On Windows, $(REF WindowsException, std, windows, syserror). 172 */ 173 this(string filename, Mode mode, ulong size, void* address, 174 size_t window = 0) scope 175 { 176 this.filename = filename; 177 this.mMode = mode; 178 this.window = window; 179 this.address = address; 180 181 version (Windows) 182 { 183 void* p; 184 uint dwDesiredAccess2; 185 uint dwShareMode; 186 uint dwCreationDisposition; 187 uint flProtect; 188 189 final switch (mode) 190 { 191 case Mode.read: 192 dwDesiredAccess2 = GENERIC_READ; 193 dwShareMode = FILE_SHARE_READ; 194 dwCreationDisposition = OPEN_EXISTING; 195 flProtect = PAGE_READONLY; 196 dwDesiredAccess = FILE_MAP_READ; 197 break; 198 199 case Mode.readWriteNew: 200 assert(size != 0); 201 dwDesiredAccess2 = GENERIC_READ | GENERIC_WRITE; 202 dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; 203 dwCreationDisposition = CREATE_ALWAYS; 204 flProtect = PAGE_READWRITE; 205 dwDesiredAccess = FILE_MAP_WRITE; 206 break; 207 208 case Mode.readWrite: 209 dwDesiredAccess2 = GENERIC_READ | GENERIC_WRITE; 210 dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; 211 dwCreationDisposition = OPEN_ALWAYS; 212 flProtect = PAGE_READWRITE; 213 dwDesiredAccess = FILE_MAP_WRITE; 214 break; 215 216 case Mode.readCopyOnWrite: 217 dwDesiredAccess2 = GENERIC_READ | GENERIC_WRITE; 218 dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE; 219 dwCreationDisposition = OPEN_EXISTING; 220 flProtect = PAGE_WRITECOPY; 221 dwDesiredAccess = FILE_MAP_COPY; 222 break; 223 } 224 225 if (filename != null) 226 { 227 hFile = CreateFileW(filename.tempCStringW(), 228 dwDesiredAccess2, 229 dwShareMode, 230 null, 231 dwCreationDisposition, 232 FILE_ATTRIBUTE_NORMAL, 233 cast(HANDLE) null); 234 wenforce(hFile != INVALID_HANDLE_VALUE, "CreateFileW"); 235 } 236 else 237 hFile = INVALID_HANDLE_VALUE; 238 239 scope(failure) 240 { 241 if (hFile != INVALID_HANDLE_VALUE) 242 { 243 CloseHandle(hFile); 244 hFile = INVALID_HANDLE_VALUE; 245 } 246 } 247 248 int hi = cast(int)(size >> 32); 249 hFileMap = CreateFileMappingW(hFile, null, flProtect, 250 hi, cast(uint) size, null); 251 wenforce(hFileMap, "CreateFileMapping"); 252 scope(failure) 253 { 254 CloseHandle(hFileMap); 255 hFileMap = null; 256 } 257 258 if (size == 0 && filename != null) 259 { 260 uint sizehi; 261 uint sizelow = GetFileSize(hFile, &sizehi); 262 wenforce(sizelow != INVALID_FILE_SIZE || GetLastError() != ERROR_SUCCESS, 263 "GetFileSize"); 264 size = (cast(ulong) sizehi << 32) + sizelow; 265 } 266 this.size = size; 267 268 size_t initial_map = (window && 2*window<size) 269 ? 2*window : cast(size_t) size; 270 p = MapViewOfFileEx(hFileMap, dwDesiredAccess, 0, 0, 271 initial_map, address); 272 wenforce(p, "MapViewOfFileEx"); 273 data = p[0 .. initial_map]; 274 275 debug (MMFILE) printf("MmFile.this(): p = %p, size = %d\n", p, size); 276 } 277 else version (Posix) 278 { 279 void* p; 280 int oflag; 281 int fmode; 282 283 final switch (mode) 284 { 285 case Mode.read: 286 flags = MAP_SHARED; 287 prot = PROT_READ; 288 oflag = O_RDONLY; 289 fmode = 0; 290 break; 291 292 case Mode.readWriteNew: 293 assert(size != 0); 294 flags = MAP_SHARED; 295 prot = PROT_READ | PROT_WRITE; 296 oflag = O_CREAT | O_RDWR | O_TRUNC; 297 fmode = octal!660; 298 break; 299 300 case Mode.readWrite: 301 flags = MAP_SHARED; 302 prot = PROT_READ | PROT_WRITE; 303 oflag = O_CREAT | O_RDWR; 304 fmode = octal!660; 305 break; 306 307 case Mode.readCopyOnWrite: 308 flags = MAP_PRIVATE; 309 prot = PROT_READ | PROT_WRITE; 310 oflag = O_RDWR; 311 fmode = 0; 312 break; 313 } 314 315 if (filename.length) 316 { 317 fd = .open(filename.tempCString(), oflag, fmode); 318 errnoEnforce(fd != -1, "Could not open file "~filename); 319 320 stat_t statbuf; 321 if (fstat(fd, &statbuf)) 322 { 323 //printf("\tfstat error, errno = %d\n", errno); 324 .close(fd); 325 fd = -1; 326 errnoEnforce(false, "Could not stat file "~filename); 327 } 328 329 if (prot & PROT_WRITE && size > statbuf.st_size) 330 { 331 // Need to make the file size bytes big 332 .lseek(fd, cast(off_t)(size - 1), SEEK_SET); 333 char c = 0; 334 core.sys.posix.unistd.write(fd, &c, 1); 335 } 336 else if (prot & PROT_READ && size == 0) 337 size = statbuf.st_size; 338 } 339 else 340 { 341 fd = -1; 342 flags |= MAP_ANON; 343 } 344 this.size = size; 345 size_t initial_map = (window && 2*window<size) 346 ? 2*window : cast(size_t) size; 347 p = mmap(address, initial_map, prot, flags, fd, 0); 348 if (p == MAP_FAILED) 349 { 350 if (fd != -1) 351 { 352 .close(fd); 353 fd = -1; 354 } 355 errnoEnforce(false, "Could not map file "~filename); 356 } 357 358 data = p[0 .. initial_map]; 359 } 360 else 361 { 362 static assert(0); 363 } 364 } 365 366 /** 367 * Flushes pending output and closes the memory mapped file. 368 */ 369 ~this() scope 370 { 371 debug (MMFILE) printf("MmFile.~this()\n"); 372 unmap(); 373 data = null; 374 version (Windows) 375 { 376 wenforce(hFileMap == null || CloseHandle(hFileMap) == TRUE, 377 "Could not close file handle"); 378 hFileMap = null; 379 380 wenforce(!hFile || hFile == INVALID_HANDLE_VALUE 381 || CloseHandle(hFile) == TRUE, 382 "Could not close handle"); 383 hFile = INVALID_HANDLE_VALUE; 384 } 385 else version (Posix) 386 { 387 version (linux) 388 { 389 if (file !is File.init) 390 { 391 // The File destructor will close the file, 392 // if it is the only remaining reference. 393 return; 394 } 395 } 396 errnoEnforce(fd == -1 || fd <= 2 397 || .close(fd) != -1, 398 "Could not close handle"); 399 fd = -1; 400 } 401 else 402 { 403 static assert(0); 404 } 405 } 406 407 /* Flush any pending output. 408 */ 409 void flush() 410 { 411 debug (MMFILE) printf("MmFile.flush()\n"); 412 version (Windows) 413 { 414 FlushViewOfFile(data.ptr, data.length); 415 } 416 else version (Posix) 417 { 418 int i; 419 i = msync(cast(void*) data, data.length, MS_SYNC); // sys/mman.h 420 errnoEnforce(i == 0, "msync failed"); 421 } 422 else 423 { 424 static assert(0); 425 } 426 } 427 428 /** 429 * Gives size in bytes of the memory mapped file. 430 */ 431 @property ulong length() const 432 { 433 debug (MMFILE) printf("MmFile.length()\n"); 434 return size; 435 } 436 437 /** 438 * Forwards `length`. 439 */ 440 alias opDollar = length; 441 442 /** 443 * Read-only property returning the file mode. 444 */ 445 Mode mode() 446 { 447 debug (MMFILE) printf("MmFile.mode()\n"); 448 return mMode; 449 } 450 451 /** 452 * Returns entire file contents as an array. 453 */ 454 void[] opSlice() 455 { 456 debug (MMFILE) printf("MmFile.opSlice()\n"); 457 return opSlice(0,size); 458 } 459 460 /** 461 * Returns slice of file contents as an array. 462 */ 463 void[] opSlice(ulong i1, ulong i2) 464 { 465 debug (MMFILE) printf("MmFile.opSlice(%lld, %lld)\n", i1, i2); 466 ensureMapped(i1,i2); 467 size_t off1 = cast(size_t)(i1-start); 468 size_t off2 = cast(size_t)(i2-start); 469 return data[off1 .. off2]; 470 } 471 472 /** 473 * Returns byte at index i in file. 474 */ 475 ubyte opIndex(ulong i) 476 { 477 debug (MMFILE) printf("MmFile.opIndex(%lld)\n", i); 478 ensureMapped(i); 479 size_t off = cast(size_t)(i-start); 480 return (cast(ubyte[]) data)[off]; 481 } 482 483 /** 484 * Sets and returns byte at index i in file to value. 485 */ 486 ubyte opIndexAssign(ubyte value, ulong i) 487 { 488 debug (MMFILE) printf("MmFile.opIndex(%lld, %d)\n", i, value); 489 ensureMapped(i); 490 size_t off = cast(size_t)(i-start); 491 return (cast(ubyte[]) data)[off] = value; 492 } 493 494 495 // return true if the given position is currently mapped 496 private int mapped(ulong i) 497 { 498 debug (MMFILE) printf("MmFile.mapped(%lld, %lld, %d)\n", i,start, 499 data.length); 500 return i >= start && i < start+data.length; 501 } 502 503 // unmap the current range 504 private void unmap() 505 { 506 debug (MMFILE) printf("MmFile.unmap()\n"); 507 version (Windows) 508 { 509 wenforce(!data.ptr || UnmapViewOfFile(data.ptr) != FALSE, "UnmapViewOfFile"); 510 } 511 else 512 { 513 errnoEnforce(!data.ptr || munmap(cast(void*) data, data.length) == 0, 514 "munmap failed"); 515 } 516 data = null; 517 } 518 519 // map range 520 private void map(ulong start, size_t len) 521 { 522 debug (MMFILE) printf("MmFile.map(%lld, %d)\n", start, len); 523 void* p; 524 if (start+len > size) 525 len = cast(size_t)(size-start); 526 version (Windows) 527 { 528 uint hi = cast(uint)(start >> 32); 529 p = MapViewOfFileEx(hFileMap, dwDesiredAccess, hi, cast(uint) start, len, address); 530 wenforce(p, "MapViewOfFileEx"); 531 } 532 else 533 { 534 p = mmap(address, len, prot, flags, fd, cast(off_t) start); 535 errnoEnforce(p != MAP_FAILED); 536 } 537 data = p[0 .. len]; 538 this.start = start; 539 } 540 541 // ensure a given position is mapped 542 private void ensureMapped(ulong i) 543 { 544 debug (MMFILE) printf("MmFile.ensureMapped(%lld)\n", i); 545 if (!mapped(i)) 546 { 547 unmap(); 548 if (window == 0) 549 { 550 map(0,cast(size_t) size); 551 } 552 else 553 { 554 ulong block = i/window; 555 if (block == 0) 556 map(0,2*window); 557 else 558 map(window*(block-1),3*window); 559 } 560 } 561 } 562 563 // ensure a given range is mapped 564 private void ensureMapped(ulong i, ulong j) 565 { 566 debug (MMFILE) printf("MmFile.ensureMapped(%lld, %lld)\n", i, j); 567 if (!mapped(i) || !mapped(j-1)) 568 { 569 unmap(); 570 if (window == 0) 571 { 572 map(0,cast(size_t) size); 573 } 574 else 575 { 576 ulong iblock = i/window; 577 ulong jblock = (j-1)/window; 578 if (iblock == 0) 579 { 580 map(0,cast(size_t)(window*(jblock+2))); 581 } 582 else 583 { 584 map(window*(iblock-1),cast(size_t)(window*(jblock-iblock+3))); 585 } 586 } 587 } 588 } 589 590 private: 591 string filename; 592 void[] data; 593 ulong start; 594 size_t window; 595 ulong size; 596 Mode mMode; 597 void* address; 598 version (linux) File file; 599 600 version (Windows) 601 { 602 HANDLE hFile = INVALID_HANDLE_VALUE; 603 HANDLE hFileMap = null; 604 uint dwDesiredAccess; 605 } 606 else version (Posix) 607 { 608 int fd; 609 int prot; 610 int flags; 611 int fmode; 612 } 613 else 614 { 615 static assert(0); 616 } 617 618 // Report error, where errno gives the error number 619 // void errNo() 620 // { 621 // version (Windows) 622 // { 623 // throw new FileException(filename, GetLastError()); 624 // } 625 // else version (linux) 626 // { 627 // throw new FileException(filename, errno); 628 // } 629 // else 630 // { 631 // static assert(0); 632 // } 633 // } 634 } 635 636 @system unittest 637 { 638 import core.memory : GC; 639 import std.file : deleteme; 640 641 const size_t K = 1024; 642 size_t win = 64*K; // assume the page size is 64K 643 version (Windows) 644 { 645 /+ these aren't defined in core.sys.windows.windows so let's use default 646 SYSTEM_INFO sysinfo; 647 GetSystemInfo(&sysinfo); 648 win = sysinfo.dwAllocationGranularity; 649 +/ 650 } 651 else version (Posix) 652 { 653 import core.sys.posix.unistd; 654 win = cast(size_t) sysconf(_SC_PAGESIZE); 655 } 656 string test_file = std.file.deleteme ~ "-testing.txt"; 657 MmFile mf = new MmFile(test_file,MmFile.Mode.readWriteNew, 658 100*K,null,win); 659 ubyte[] str = cast(ubyte[])"1234567890"; 660 ubyte[] data = cast(ubyte[]) mf[0 .. 10]; 661 data[] = str[]; 662 assert( mf[0 .. 10] == str ); 663 data = cast(ubyte[]) mf[50 .. 60]; 664 data[] = str[]; 665 assert( mf[50 .. 60] == str ); 666 ubyte[] data2 = cast(ubyte[]) mf[20*K .. 60*K]; 667 assert( data2.length == 40*K ); 668 assert( data2[$-1] == 0 ); 669 mf[100*K-1] = cast(ubyte)'b'; 670 data2 = cast(ubyte[]) mf[21*K .. 100*K]; 671 assert( data2.length == 79*K ); 672 assert( data2[$-1] == 'b' ); 673 674 destroy(mf); 675 676 std.file.remove(test_file); 677 // Create anonymous mapping 678 auto test = new MmFile(null, MmFile.Mode.readWriteNew, 1024*1024, null); 679 } 680 681 version (linux) 682 @system unittest // https://issues.dlang.org/show_bug.cgi?id=14868 683 { 684 import std.file : deleteme; 685 import std.typecons : scoped; 686 687 // Test retaining ownership of File/fd 688 689 auto fn = std.file.deleteme ~ "-testing.txt"; 690 scope(exit) std.file.remove(fn); 691 File(fn, "wb").writeln("Testing!"); 692 scoped!MmFile(File(fn)); 693 694 // Test that unique ownership of File actually leads to the fd being closed 695 696 auto f = File(fn); 697 auto fd = f.fileno; 698 { 699 auto mf = scoped!MmFile(f); 700 f = File.init; 701 } 702 assert(.close(fd) == -1); 703 } 704 705 // https://issues.dlang.org/show_bug.cgi?id=14994 706 // https://issues.dlang.org/show_bug.cgi?id=14995 707 @system unittest 708 { 709 import std.file : deleteme; 710 import std.typecons : scoped; 711 712 // Zero-length map may or may not be valid on OSX and NetBSD 713 version (OSX) 714 import std.exception : verifyThrown = collectException; 715 version (NetBSD) 716 import std.exception : verifyThrown = collectException; 717 else 718 import std.exception : verifyThrown = assertThrown; 719 720 auto fn = std.file.deleteme ~ "-testing.txt"; 721 scope(exit) std.file.remove(fn); 722 verifyThrown(scoped!MmFile(fn, MmFile.Mode.readWrite, 0, null)); 723 } 724 725 @system unittest 726 { 727 MmFile shar = new MmFile(null, MmFile.Mode.readWrite, 10, null, 0); 728 void[] output = shar[0 .. $]; 729 } 730 731 @system unittest 732 { 733 import std.file : deleteme; 734 auto name = std.file.deleteme ~ "-test.tmp"; 735 scope(exit) std.file.remove(name); 736 737 std.file.write(name, "abcd"); 738 { 739 scope MmFile mmf = new MmFile(name); 740 string p; 741 742 assert(mmf[0] == 'a'); 743 p = cast(string) mmf[]; 744 assert(p[1] == 'b'); 745 p = cast(string) mmf[0 .. 4]; 746 assert(p[2] == 'c'); 747 } 748 { 749 scope MmFile mmf = new MmFile(name, MmFile.Mode.read, 0, null); 750 string p; 751 752 assert(mmf[0] == 'a'); 753 p = cast(string) mmf[]; 754 assert(mmf.length == 4); 755 assert(p[1] == 'b'); 756 p = cast(string) mmf[0 .. 4]; 757 assert(p[2] == 'c'); 758 } 759 std.file.remove(name); 760 { 761 scope MmFile mmf = new MmFile(name, MmFile.Mode.readWriteNew, 4, null); 762 char[] p = cast(char[]) mmf[]; 763 p[] = "1234"; 764 mmf[3] = '5'; 765 assert(mmf[2] == '3'); 766 assert(mmf[3] == '5'); 767 } 768 { 769 string p = cast(string) std.file.read(name); 770 assert(p[] == "1235"); 771 } 772 { 773 scope MmFile mmf = new MmFile(name, MmFile.Mode.readWriteNew, 4, null); 774 char[] p = cast(char[]) mmf[]; 775 p[] = "5678"; 776 mmf[3] = '5'; 777 assert(mmf[2] == '7'); 778 assert(mmf[3] == '5'); 779 assert(cast(string) mmf[] == "5675"); 780 } 781 { 782 string p = cast(string) std.file.read(name); 783 assert(p[] == "5675"); 784 } 785 { 786 scope MmFile mmf = new MmFile(name, MmFile.Mode.readWrite, 4, null); 787 char[] p = cast(char[]) mmf[]; 788 assert(cast(char[]) mmf[] == "5675"); 789 p[] = "9102"; 790 mmf[2] = '5'; 791 assert(cast(string) mmf[] == "9152"); 792 } 793 { 794 string p = cast(string) std.file.read(name); 795 assert(p[] == "9152"); 796 } 797 std.file.remove(name); 798 { 799 scope MmFile mmf = new MmFile(name, MmFile.Mode.readWrite, 4, null); 800 char[] p = cast(char[]) mmf[]; 801 p[] = "abcd"; 802 mmf[2] = '5'; 803 assert(cast(string) mmf[] == "ab5d"); 804 } 805 { 806 string p = cast(string) std.file.read(name); 807 assert(p[] == "ab5d"); 808 } 809 { 810 scope MmFile mmf = new MmFile(name, MmFile.Mode.readCopyOnWrite, 4, null); 811 char[] p = cast(char[]) mmf[]; 812 assert(cast(string) mmf[] == "ab5d"); 813 p[] = "9102"; 814 mmf[2] = '5'; 815 assert(cast(string) mmf[] == "9152"); 816 } 817 { 818 string p = cast(string) std.file.read(name); 819 assert(p[] == "ab5d"); 820 } 821 }