1 /* 2 Copyright (c) 2014-2021 Martin Cejp 3 4 Boost Software License - Version 1.0 - August 17th, 2003 5 6 Permission is hereby granted, free of charge, to any person or organization 7 obtaining a copy of the software and accompanying documentation covered by 8 this license (the "Software") to use, reproduce, display, distribute, 9 execute, and transmit the Software, and to prepare derivative works of the 10 Software, and to permit third-parties to whom the Software is furnished to 11 do so, all subject to the following: 12 13 The copyright notices in the Software and this entire statement, including 14 the above license grant, this restriction and the following disclaimer, 15 must be included in all copies of the Software, in whole or in part, and 16 all derivative works of the Software, unless such copies or derivative 17 works are solely in the form of machine-executable object code generated by 18 a source language processor. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 DEALINGS IN THE SOFTWARE. 27 */ 28 29 /** 30 * Copyright: Martin Cejp 2014-2021. 31 * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0). 32 * Authors: Martin Cejp 33 */ 34 module dlib.filesystem.local; 35 36 import std.array; 37 import std.conv; 38 import std.datetime; 39 import std.path; 40 import std.range; 41 import std.stdio; 42 import std..string; 43 44 import dlib.core.stream; 45 import dlib.filesystem.filesystem; 46 import dlib.filesystem.dirrange; 47 48 version (Posix) 49 { 50 import dlib.filesystem.posix.common; 51 import dlib.filesystem.posix.directory; 52 import dlib.filesystem.posix.file; 53 } 54 else version (Windows) 55 { 56 import dlib.filesystem.windows.common; 57 import dlib.filesystem.windows.directory; 58 import dlib.filesystem.windows.file; 59 } 60 61 // TODO: Should probably check for FILE_ATTRIBUTE_REPARSE_POINT before recursing 62 63 /// LocalFileSystem 64 class LocalFileSystem : FileSystem 65 { 66 override InputStream openForInput(string filename) 67 { 68 return cast(InputStream) openFile(filename, read, 0); 69 } 70 71 override OutputStream openForOutput(string filename, uint creationFlags) 72 { 73 return cast(OutputStream) openFile(filename, write, creationFlags); 74 } 75 76 override IOStream openForIO(string filename, uint creationFlags) 77 { 78 return openFile(filename, read | write, creationFlags); 79 } 80 81 override bool createDir(string path, bool recursive) 82 { 83 import std.algorithm; 84 85 if (recursive) 86 { 87 ptrdiff_t index = max(path.lastIndexOf('/'), path.lastIndexOf('\\')); 88 89 if (index != -1) 90 createDir(path[0..index], true); 91 } 92 93 version(Posix) 94 { 95 return mkdir(toStringz(path), access_0755) == 0; 96 } 97 else version (Windows) 98 { 99 return CreateDirectoryW(toUTF16z(path), null) != 0; 100 } 101 else 102 throw new Exception("Not implemented."); 103 } 104 105 override Directory openDir(string path) 106 { 107 version(Posix) 108 { 109 DIR* d = opendir(!path.empty ? toStringz(path) : "."); 110 111 if (d == null) 112 return null; 113 else 114 return new PosixDirectory(this, d, !path.empty ? path ~ "/" : ""); 115 } 116 else version(Windows) 117 { 118 string npath = !path.empty ? buildNormalizedPath(path) : "."; 119 DWORD attributes = GetFileAttributesW(toUTF16z(npath)); 120 121 if (attributes == INVALID_FILE_ATTRIBUTES) 122 return null; 123 124 if (attributes & FILE_ATTRIBUTE_DIRECTORY) 125 return new WindowsDirectory(this, npath, !path.empty ? path ~ "/" : ""); 126 else 127 return null; 128 } 129 else 130 throw new Exception("Not implemented."); 131 } 132 133 override bool stat(string path, out FileStat stat_out) 134 { 135 version(Posix) 136 { 137 stat_t st; 138 139 if (stat_(toStringz(path), &st) != 0) 140 return false; 141 142 stat_out.isFile = S_ISREG(st.st_mode); 143 stat_out.isDirectory = S_ISDIR(st.st_mode); 144 145 stat_out.sizeInBytes = st.st_size; 146 stat_out.creationTimestamp = SysTime(unixTimeToStdTime(st.st_ctime)); 147 auto modificationStdTime = unixTimeToStdTime(st.st_mtime); 148 static if (is(typeof(st.st_mtimensec))) 149 { 150 modificationStdTime += st.st_mtimensec / 100; 151 } 152 stat_out.modificationTimestamp = SysTime(modificationStdTime); 153 154 if ((st.st_mode & S_IRUSR) | (st.st_mode & S_IRGRP) | (st.st_mode & S_IROTH)) 155 stat_out.permissions |= PRead; 156 if ((st.st_mode & S_IWUSR) | (st.st_mode & S_IWGRP) | (st.st_mode & S_IWOTH)) 157 stat_out.permissions |= PWrite; 158 if ((st.st_mode & S_IXUSR) | (st.st_mode & S_IXGRP) | (st.st_mode & S_IXOTH)) 159 stat_out.permissions |= PExecute; 160 161 return true; 162 } 163 else version(Windows) 164 { 165 WIN32_FILE_ATTRIBUTE_DATA data; 166 167 auto p = toUTF16z(path); 168 169 if (!GetFileAttributesExW(p, GET_FILEEX_INFO_LEVELS.GetFileExInfoStandard, &data)) 170 return false; 171 172 if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 173 stat_out.isDirectory = true; 174 else 175 stat_out.isFile = true; 176 177 stat_out.sizeInBytes = (cast(FileSize) data.nFileSizeHigh << 32) | data.nFileSizeLow; 178 stat_out.creationTimestamp = SysTime(FILETIMEToStdTime(&data.ftCreationTime)); 179 stat_out.modificationTimestamp = SysTime(FILETIMEToStdTime(&data.ftLastWriteTime)); 180 181 stat_out.permissions = 0; 182 183 PACL pacl; 184 PSECURITY_DESCRIPTOR secDesc; 185 TRUSTEE_W trustee; 186 trustee.pMultipleTrustee = null; 187 trustee.MultipleTrusteeOperation = MULTIPLE_TRUSTEE_OPERATION.NO_MULTIPLE_TRUSTEE; 188 trustee.TrusteeForm = TRUSTEE_FORM.TRUSTEE_IS_NAME; 189 trustee.TrusteeType = TRUSTEE_TYPE.TRUSTEE_IS_UNKNOWN; 190 trustee.ptstrName = cast(wchar*)"CURRENT_USER"w.ptr; 191 GetNamedSecurityInfoW(cast(wchar*)p, SE_OBJECT_TYPE.SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, null, null, &pacl, null, &secDesc); 192 if (pacl) 193 { 194 uint access; 195 GetEffectiveRightsFromAcl(pacl, &trustee, &access); 196 197 if (access & ACTRL_FILE_READ) 198 stat_out.permissions |= PRead; 199 if ((access & ACTRL_FILE_WRITE) && !(data.dwFileAttributes & FILE_ATTRIBUTE_READONLY)) 200 stat_out.permissions |= PWrite; 201 if (access & ACTRL_FILE_EXECUTE) 202 stat_out.permissions |= PExecute; 203 } 204 205 return true; 206 } 207 else 208 throw new Exception("Not implemented."); 209 } 210 211 /* 212 override bool move(string path, string newPath) 213 { 214 // TODO: should we allow newPath to actually be a directory? 215 216 return rename(toStringz(path), toStringz(newPath)) == 0; 217 } 218 */ 219 220 override bool remove(string path, bool recursive) 221 { 222 FileStat stat; 223 224 if (!this.stat(path, stat)) 225 return false; 226 227 return remove(path, stat.isDirectory, recursive); 228 } 229 230 private: 231 version(Posix) 232 { 233 enum access_0644 = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; 234 enum access_0755 = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH; 235 } 236 237 IOStream openFile(string filename, uint accessFlags, uint creationFlags) 238 { 239 version(Posix) 240 { 241 int flags; 242 243 switch (accessFlags & (read | write)) 244 { 245 case read: flags = O_RDONLY; break; 246 case write: flags = O_WRONLY; break; 247 case (read | write): flags = O_RDWR; break; 248 default: flags = 0; break; 249 } 250 251 if (creationFlags & FileSystem.create) 252 flags |= O_CREAT; 253 254 if (creationFlags & FileSystem.truncate) 255 flags |= O_TRUNC; 256 257 int fd = open(toStringz(filename), flags, access_0644); 258 259 if (fd < 0) 260 return null; 261 else 262 return new PosixFile(fd, accessFlags); 263 } 264 else version(Windows) 265 { 266 DWORD access = 0; 267 268 if (accessFlags & read) 269 access |= GENERIC_READ; 270 271 if (accessFlags & write) 272 access |= GENERIC_WRITE; 273 274 DWORD creationMode; 275 276 switch (creationFlags & (create | truncate)) 277 { 278 case 0: creationMode = OPEN_EXISTING; break; 279 case create: creationMode = OPEN_ALWAYS; break; 280 case truncate: creationMode = TRUNCATE_EXISTING; break; 281 case (create | truncate): creationMode = CREATE_ALWAYS; break; 282 default: creationMode = OPEN_EXISTING; break; 283 } 284 285 HANDLE file = CreateFileW(toUTF16z(filename), access, FILE_SHARE_READ, null, creationMode, 286 FILE_ATTRIBUTE_NORMAL, null); 287 288 if (file == INVALID_HANDLE_VALUE) 289 return null; 290 else 291 return new WindowsFile(file, accessFlags); 292 } 293 else 294 throw new Exception("Not implemented."); 295 } 296 297 bool remove(string path, bool isDirectory, bool recursive) 298 { 299 if (isDirectory && recursive) 300 { 301 // Remove contents 302 auto dir = openDir(path); 303 304 try 305 { 306 foreach (entry; dir.contents) 307 remove(path ~ "/" ~ entry.name, entry.isDirectory, recursive); 308 } 309 finally 310 { 311 dir.close(); 312 } 313 } 314 315 version(Posix) 316 { 317 if (isDirectory) 318 return rmdir(toStringz(path)) == 0; 319 else 320 return std.stdio.remove(toStringz(path)) == 0; 321 } 322 else version(Windows) 323 { 324 if (isDirectory) 325 return RemoveDirectoryW(toUTF16z(path)) != 0; 326 else 327 return DeleteFileW(toUTF16z(path)) != 0; 328 } 329 else 330 throw new Exception("Not implemented."); 331 } 332 } 333 334 private ReadOnlyFileSystem rofs; 335 private FileSystem fs; 336 337 static this() 338 { 339 // decouple dependency from the rest of this module 340 import dlib.filesystem.local; 341 342 setFileSystem(new LocalFileSystem); 343 } 344 345 void setFileSystem(FileSystem fs_) 346 { 347 rofs = fs_; 348 fs = fs_; 349 } 350 351 void setFileSystemReadOnly(ReadOnlyFileSystem rofs_) 352 { 353 rofs = rofs_; 354 fs = null; 355 } 356 357 // ReadOnlyFileSystem 358 359 bool stat(string filename, out FileStat stat) 360 { 361 return rofs.stat(filename, stat); 362 } 363 364 InputStream openForInput(string filename) 365 { 366 InputStream ins = rofs.openForInput(filename); 367 368 if (ins is null) 369 throw new Exception("Failed to open '" ~ filename ~ "'"); 370 371 return ins; 372 } 373 374 Directory openDir(string path) 375 { 376 return rofs.openDir(path); 377 } 378 379 InputRange!DirEntry findFiles(string baseDir, bool recursive) 380 { 381 return dlib.filesystem.filesystem.findFiles(rofs, baseDir, recursive); 382 } 383 384 // FileSystem 385 386 OutputStream openForOutput(string filename, uint creationFlags = FileSystem.create | FileSystem.truncate) 387 { 388 OutputStream outs = fs.openForOutput(filename, creationFlags); 389 390 if (outs is null) 391 throw new Exception("Failed to open '" ~ filename ~ "' for writing"); 392 393 return outs; 394 } 395 396 IOStream openForIO(string filename, uint creationFlags) 397 { 398 IOStream ios = fs.openForIO(filename, creationFlags); 399 400 if (ios is null) 401 throw new Exception("Failed to open '" ~ filename ~ "' for writing"); 402 403 return ios; 404 } 405 406 bool createDir(string path, bool recursive) 407 { 408 return fs.createDir(path, recursive); 409 } 410 411 /* 412 bool move(string path, string newPath) 413 { 414 return fs.move(path, newPath); 415 } 416 */ 417 418 bool remove(string path, bool recursive) 419 { 420 return fs.remove(path, recursive); 421 } 422 423 unittest 424 { 425 // TODO: test >4GiB files 426 427 import std.algorithm; 428 import std.file; 429 430 alias remove = dlib.filesystem.local.remove; 431 432 remove("tests/test_data", true); 433 assert(openDir("tests/test_data") is null); 434 435 assert(createDir("tests/test_data/main", true)); 436 437 enum dir = "tests"; 438 auto d = openDir(dir); 439 440 try 441 { 442 chdir(dir); 443 auto expected = dirEntries("", SpanMode.shallow) 444 .filter!(e => e.isFile) 445 .array; 446 size_t i; 447 chdir(".."); 448 449 foreach (entry; d.contents) 450 { 451 if (entry.isFile) 452 { 453 assert(expected[i] == entry.name); 454 ++i; 455 } 456 } 457 } 458 finally 459 { 460 d.close(); 461 } 462 463 // 464 OutputStream outp = openForOutput("tests/test_data/main/hello_world.txt", FileSystem.create | FileSystem.truncate); 465 string expected = "Hello, World!\n"; 466 assert(outp); 467 468 try 469 { 470 assert(outp.writeArray(expected)); 471 } 472 finally 473 { 474 outp.close(); 475 } 476 477 // 478 InputStream inp = openForInput("tests/test_data/main/hello_world.txt"); 479 assert(inp); 480 481 try 482 { 483 while (inp.readable) 484 { 485 char[1] buffer; 486 487 auto have = inp.readBytes(buffer.ptr, buffer.length); 488 assert(buffer[0..have] == expected[0..have]); 489 expected.popFrontN(have); 490 } 491 } 492 finally 493 { 494 inp.close(); 495 } 496 } 497 498 unittest 499 { 500 import std.algorithm; 501 import std.file; 502 503 auto expected = dirEntries("", SpanMode.depth) 504 .filter!(e => e.isFile) 505 .filter!(e => e.name.baseName.endsWith(".d")) 506 .map!(e => e.name.replace("\\", "/")) 507 .array; 508 size_t i; 509 510 foreach (entry; findFiles("", true) 511 .filter!(entry => entry.isFile) 512 .filter!(e => e.name.baseName.globMatch("*.d"))) 513 { 514 FileStat stat_; 515 assert(stat(entry.name, stat_)); // make sure we're getting the expected path 516 assert(expected[i] == entry.name); 517 assert(stat_.sizeInBytes == expected[i].getSize()); 518 519 SysTime modificationTime, accessTime; 520 expected[i].getTimes(accessTime, modificationTime); 521 assert(modificationTime == stat_.modificationTimestamp); 522 523 ++i; 524 } 525 }