| author | |
| committer | |
| log | c95e2e65fac6afec5c57f6716b1f4e8e7a7292fb |
| tree | 818aaacaa27fb754107fb0f5ef5a19cb80c94831 |
| parent | e357550610aef476390ed7191eeaf7597a8e9d53 |
4 files changed, 2567 insertions(+), 2531 deletions(-)
CMakeLists.txt+1| ... | ... | @@ -247,6 +247,7 @@ set(ZIG_STAGE2_SOURCES |
| 247 | 247 | "${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig" |
| 248 | 248 | "${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig" |
| 249 | 249 | "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" |
| 250 | "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" | |
| 250 | 251 | "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" |
| 251 | 252 | "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" |
| 252 | 253 | "${CMAKE_SOURCE_DIR}/lib/std/fs/path.zig" |
lib/std/fs.zig+31-2529| ... | ... | @@ -11,6 +11,8 @@ const math = std.math; |
| 11 | 11 | |
| 12 | 12 | const is_darwin = builtin.os.tag.isDarwin(); |
| 13 | 13 | |
| 14 | pub const Dir = @import("fs/Dir.zig"); | |
| 15 | ||
| 14 | 16 | pub const has_executable_bit = switch (builtin.os.tag) { |
| 15 | 17 | .windows, .wasi => false, |
| 16 | 18 | else => true, |
| ... | ... | @@ -120,24 +122,14 @@ pub fn atomicSymLink(allocator: Allocator, existing_path: []const u8, new_path: |
| 120 | 122 | } |
| 121 | 123 | } |
| 122 | 124 | |
| 123 | pub const PrevStatus = enum { | |
| 124 | stale, | |
| 125 | fresh, | |
| 126 | }; | |
| 127 | ||
| 128 | pub const CopyFileOptions = struct { | |
| 129 | /// When this is `null` the mode is copied from the source file. | |
| 130 | override_mode: ?File.Mode = null, | |
| 131 | }; | |
| 132 | ||
| 133 | 125 | /// Same as `Dir.updateFile`, except asserts that both `source_path` and `dest_path` |
| 134 | 126 | /// are absolute. See `Dir.updateFile` for a function that operates on both |
| 135 | 127 | /// absolute and relative paths. |
| 136 | 128 | pub fn updateFileAbsolute( |
| 137 | 129 | source_path: []const u8, |
| 138 | 130 | dest_path: []const u8, |
| 139 | args: CopyFileOptions, | |
| 140 | ) !PrevStatus { | |
| 131 | args: Dir.CopyFileOptions, | |
| 132 | ) !Dir.PrevStatus { | |
| 141 | 133 | assert(path.isAbsolute(source_path)); |
| 142 | 134 | assert(path.isAbsolute(dest_path)); |
| 143 | 135 | const my_cwd = cwd(); |
| ... | ... | @@ -147,7 +139,11 @@ pub fn updateFileAbsolute( |
| 147 | 139 | /// Same as `Dir.copyFile`, except asserts that both `source_path` and `dest_path` |
| 148 | 140 | /// are absolute. See `Dir.copyFile` for a function that operates on both |
| 149 | 141 | /// absolute and relative paths. |
| 150 | pub fn copyFileAbsolute(source_path: []const u8, dest_path: []const u8, args: CopyFileOptions) !void { | |
| 142 | pub fn copyFileAbsolute( | |
| 143 | source_path: []const u8, | |
| 144 | dest_path: []const u8, | |
| 145 | args: Dir.CopyFileOptions, | |
| 146 | ) !void { | |
| 151 | 147 | assert(path.isAbsolute(source_path)); |
| 152 | 148 | assert(path.isAbsolute(dest_path)); |
| 153 | 149 | const my_cwd = cwd(); |
| ... | ... | @@ -164,7 +160,7 @@ pub const AtomicFile = struct { |
| 164 | 160 | close_dir_on_deinit: bool, |
| 165 | 161 | dir: Dir, |
| 166 | 162 | |
| 167 | const InitError = File.OpenError; | |
| 163 | pub const InitError = File.OpenError; | |
| 168 | 164 | |
| 169 | 165 | const RANDOM_BYTES = 12; |
| 170 | 166 | const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES); |
| ... | ... | @@ -233,26 +229,24 @@ pub const AtomicFile = struct { |
| 233 | 229 | } |
| 234 | 230 | }; |
| 235 | 231 | |
| 236 | const default_new_dir_mode = 0o755; | |
| 237 | ||
| 238 | 232 | /// Create a new directory, based on an absolute path. |
| 239 | 233 | /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates |
| 240 | 234 | /// on both absolute and relative paths. |
| 241 | 235 | pub fn makeDirAbsolute(absolute_path: []const u8) !void { |
| 242 | 236 | assert(path.isAbsolute(absolute_path)); |
| 243 | return os.mkdir(absolute_path, default_new_dir_mode); | |
| 237 | return os.mkdir(absolute_path, Dir.default_mode); | |
| 244 | 238 | } |
| 245 | 239 | |
| 246 | 240 | /// Same as `makeDirAbsolute` except the parameter is a null-terminated UTF-8-encoded string. |
| 247 | 241 | pub fn makeDirAbsoluteZ(absolute_path_z: [*:0]const u8) !void { |
| 248 | 242 | assert(path.isAbsoluteZ(absolute_path_z)); |
| 249 | return os.mkdirZ(absolute_path_z, default_new_dir_mode); | |
| 243 | return os.mkdirZ(absolute_path_z, Dir.default_mode); | |
| 250 | 244 | } |
| 251 | 245 | |
| 252 | 246 | /// Same as `makeDirAbsolute` except the parameter is a null-terminated WTF-16-encoded string. |
| 253 | 247 | pub fn makeDirAbsoluteW(absolute_path_w: [*:0]const u16) !void { |
| 254 | 248 | assert(path.isAbsoluteWindowsW(absolute_path_w)); |
| 255 | return os.mkdirW(absolute_path_w, default_new_dir_mode); | |
| 249 | return os.mkdirW(absolute_path_w, Dir.default_mode); | |
| 256 | 250 | } |
| 257 | 251 | |
| 258 | 252 | /// Same as `Dir.deleteDir` except the path is absolute. |
| ... | ... | @@ -310,2449 +304,6 @@ pub fn renameW(old_dir: Dir, old_sub_path_w: []const u16, new_dir: Dir, new_sub_ |
| 310 | 304 | return os.renameatW(old_dir.fd, old_sub_path_w, new_dir.fd, new_sub_path_w); |
| 311 | 305 | } |
| 312 | 306 | |
| 313 | pub const Dir = struct { | |
| 314 | fd: os.fd_t, | |
| 315 | ||
| 316 | pub const Entry = struct { | |
| 317 | name: []const u8, | |
| 318 | kind: Kind, | |
| 319 | ||
| 320 | pub const Kind = File.Kind; | |
| 321 | }; | |
| 322 | ||
| 323 | const IteratorError = error{ AccessDenied, SystemResources } || os.UnexpectedError; | |
| 324 | ||
| 325 | pub const Iterator = switch (builtin.os.tag) { | |
| 326 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { | |
| 327 | dir: Dir, | |
| 328 | seek: i64, | |
| 329 | buf: [1024]u8, // TODO align(@alignOf(os.system.dirent)), | |
| 330 | index: usize, | |
| 331 | end_index: usize, | |
| 332 | first_iter: bool, | |
| 333 | ||
| 334 | const Self = @This(); | |
| 335 | ||
| 336 | pub const Error = IteratorError; | |
| 337 | ||
| 338 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 339 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 340 | pub fn next(self: *Self) Error!?Entry { | |
| 341 | switch (builtin.os.tag) { | |
| 342 | .macos, .ios => return self.nextDarwin(), | |
| 343 | .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), | |
| 344 | .solaris, .illumos => return self.nextSolaris(), | |
| 345 | else => @compileError("unimplemented"), | |
| 346 | } | |
| 347 | } | |
| 348 | ||
| 349 | fn nextDarwin(self: *Self) !?Entry { | |
| 350 | start_over: while (true) { | |
| 351 | if (self.index >= self.end_index) { | |
| 352 | if (self.first_iter) { | |
| 353 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 354 | self.first_iter = false; | |
| 355 | } | |
| 356 | const rc = os.system.__getdirentries64( | |
| 357 | self.dir.fd, | |
| 358 | &self.buf, | |
| 359 | self.buf.len, | |
| 360 | &self.seek, | |
| 361 | ); | |
| 362 | if (rc == 0) return null; | |
| 363 | if (rc < 0) { | |
| 364 | switch (os.errno(rc)) { | |
| 365 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 366 | .FAULT => unreachable, | |
| 367 | .NOTDIR => unreachable, | |
| 368 | .INVAL => unreachable, | |
| 369 | else => |err| return os.unexpectedErrno(err), | |
| 370 | } | |
| 371 | } | |
| 372 | self.index = 0; | |
| 373 | self.end_index = @as(usize, @intCast(rc)); | |
| 374 | } | |
| 375 | const darwin_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 376 | const next_index = self.index + darwin_entry.reclen(); | |
| 377 | self.index = next_index; | |
| 378 | ||
| 379 | const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; | |
| 380 | ||
| 381 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { | |
| 382 | continue :start_over; | |
| 383 | } | |
| 384 | ||
| 385 | const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { | |
| 386 | os.DT.BLK => .block_device, | |
| 387 | os.DT.CHR => .character_device, | |
| 388 | os.DT.DIR => .directory, | |
| 389 | os.DT.FIFO => .named_pipe, | |
| 390 | os.DT.LNK => .sym_link, | |
| 391 | os.DT.REG => .file, | |
| 392 | os.DT.SOCK => .unix_domain_socket, | |
| 393 | os.DT.WHT => .whiteout, | |
| 394 | else => .unknown, | |
| 395 | }; | |
| 396 | return Entry{ | |
| 397 | .name = name, | |
| 398 | .kind = entry_kind, | |
| 399 | }; | |
| 400 | } | |
| 401 | } | |
| 402 | ||
| 403 | fn nextSolaris(self: *Self) !?Entry { | |
| 404 | start_over: while (true) { | |
| 405 | if (self.index >= self.end_index) { | |
| 406 | if (self.first_iter) { | |
| 407 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 408 | self.first_iter = false; | |
| 409 | } | |
| 410 | const rc = os.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 411 | switch (os.errno(rc)) { | |
| 412 | .SUCCESS => {}, | |
| 413 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 414 | .FAULT => unreachable, | |
| 415 | .NOTDIR => unreachable, | |
| 416 | .INVAL => unreachable, | |
| 417 | else => |err| return os.unexpectedErrno(err), | |
| 418 | } | |
| 419 | if (rc == 0) return null; | |
| 420 | self.index = 0; | |
| 421 | self.end_index = @as(usize, @intCast(rc)); | |
| 422 | } | |
| 423 | const entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 424 | const next_index = self.index + entry.reclen(); | |
| 425 | self.index = next_index; | |
| 426 | ||
| 427 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); | |
| 428 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) | |
| 429 | continue :start_over; | |
| 430 | ||
| 431 | // Solaris dirent doesn't expose d_type, so we have to call stat to get it. | |
| 432 | const stat_info = os.fstatat( | |
| 433 | self.dir.fd, | |
| 434 | name, | |
| 435 | os.AT.SYMLINK_NOFOLLOW, | |
| 436 | ) catch |err| switch (err) { | |
| 437 | error.NameTooLong => unreachable, | |
| 438 | error.SymLinkLoop => unreachable, | |
| 439 | error.FileNotFound => unreachable, // lost the race | |
| 440 | else => |e| return e, | |
| 441 | }; | |
| 442 | const entry_kind: Entry.Kind = switch (stat_info.mode & os.S.IFMT) { | |
| 443 | os.S.IFIFO => .named_pipe, | |
| 444 | os.S.IFCHR => .character_device, | |
| 445 | os.S.IFDIR => .directory, | |
| 446 | os.S.IFBLK => .block_device, | |
| 447 | os.S.IFREG => .file, | |
| 448 | os.S.IFLNK => .sym_link, | |
| 449 | os.S.IFSOCK => .unix_domain_socket, | |
| 450 | os.S.IFDOOR => .door, | |
| 451 | os.S.IFPORT => .event_port, | |
| 452 | else => .unknown, | |
| 453 | }; | |
| 454 | return Entry{ | |
| 455 | .name = name, | |
| 456 | .kind = entry_kind, | |
| 457 | }; | |
| 458 | } | |
| 459 | } | |
| 460 | ||
| 461 | fn nextBsd(self: *Self) !?Entry { | |
| 462 | start_over: while (true) { | |
| 463 | if (self.index >= self.end_index) { | |
| 464 | if (self.first_iter) { | |
| 465 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 466 | self.first_iter = false; | |
| 467 | } | |
| 468 | const rc = if (builtin.os.tag == .netbsd) | |
| 469 | os.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) | |
| 470 | else | |
| 471 | os.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 472 | switch (os.errno(rc)) { | |
| 473 | .SUCCESS => {}, | |
| 474 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 475 | .FAULT => unreachable, | |
| 476 | .NOTDIR => unreachable, | |
| 477 | .INVAL => unreachable, | |
| 478 | // Introduced in freebsd 13.2: directory unlinked but still open. | |
| 479 | // To be consistent, iteration ends if the directory being iterated is deleted during iteration. | |
| 480 | .NOENT => return null, | |
| 481 | else => |err| return os.unexpectedErrno(err), | |
| 482 | } | |
| 483 | if (rc == 0) return null; | |
| 484 | self.index = 0; | |
| 485 | self.end_index = @as(usize, @intCast(rc)); | |
| 486 | } | |
| 487 | const bsd_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 488 | const next_index = self.index + bsd_entry.reclen(); | |
| 489 | self.index = next_index; | |
| 490 | ||
| 491 | const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; | |
| 492 | ||
| 493 | const skip_zero_fileno = switch (builtin.os.tag) { | |
| 494 | // d_fileno=0 is used to mark invalid entries or deleted files. | |
| 495 | .openbsd, .netbsd => true, | |
| 496 | else => false, | |
| 497 | }; | |
| 498 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or | |
| 499 | (skip_zero_fileno and bsd_entry.d_fileno == 0)) | |
| 500 | { | |
| 501 | continue :start_over; | |
| 502 | } | |
| 503 | ||
| 504 | const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { | |
| 505 | os.DT.BLK => .block_device, | |
| 506 | os.DT.CHR => .character_device, | |
| 507 | os.DT.DIR => .directory, | |
| 508 | os.DT.FIFO => .named_pipe, | |
| 509 | os.DT.LNK => .sym_link, | |
| 510 | os.DT.REG => .file, | |
| 511 | os.DT.SOCK => .unix_domain_socket, | |
| 512 | os.DT.WHT => .whiteout, | |
| 513 | else => .unknown, | |
| 514 | }; | |
| 515 | return Entry{ | |
| 516 | .name = name, | |
| 517 | .kind = entry_kind, | |
| 518 | }; | |
| 519 | } | |
| 520 | } | |
| 521 | ||
| 522 | pub fn reset(self: *Self) void { | |
| 523 | self.index = 0; | |
| 524 | self.end_index = 0; | |
| 525 | self.first_iter = true; | |
| 526 | } | |
| 527 | }, | |
| 528 | .haiku => struct { | |
| 529 | dir: Dir, | |
| 530 | buf: [1024]u8, // TODO align(@alignOf(os.dirent64)), | |
| 531 | index: usize, | |
| 532 | end_index: usize, | |
| 533 | first_iter: bool, | |
| 534 | ||
| 535 | const Self = @This(); | |
| 536 | ||
| 537 | pub const Error = IteratorError; | |
| 538 | ||
| 539 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 540 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 541 | pub fn next(self: *Self) Error!?Entry { | |
| 542 | start_over: while (true) { | |
| 543 | // TODO: find a better max | |
| 544 | const HAIKU_MAX_COUNT = 10000; | |
| 545 | if (self.index >= self.end_index) { | |
| 546 | if (self.first_iter) { | |
| 547 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 548 | self.first_iter = false; | |
| 549 | } | |
| 550 | const rc = os.system._kern_read_dir( | |
| 551 | self.dir.fd, | |
| 552 | &self.buf, | |
| 553 | self.buf.len, | |
| 554 | HAIKU_MAX_COUNT, | |
| 555 | ); | |
| 556 | if (rc == 0) return null; | |
| 557 | if (rc < 0) { | |
| 558 | switch (os.errno(rc)) { | |
| 559 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 560 | .FAULT => unreachable, | |
| 561 | .NOTDIR => unreachable, | |
| 562 | .INVAL => unreachable, | |
| 563 | else => |err| return os.unexpectedErrno(err), | |
| 564 | } | |
| 565 | } | |
| 566 | self.index = 0; | |
| 567 | self.end_index = @as(usize, @intCast(rc)); | |
| 568 | } | |
| 569 | const haiku_entry = @as(*align(1) os.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 570 | const next_index = self.index + haiku_entry.reclen(); | |
| 571 | self.index = next_index; | |
| 572 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); | |
| 573 | ||
| 574 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { | |
| 575 | continue :start_over; | |
| 576 | } | |
| 577 | ||
| 578 | var stat_info: os.Stat = undefined; | |
| 579 | const rc = os.system._kern_read_stat( | |
| 580 | self.dir.fd, | |
| 581 | &haiku_entry.d_name, | |
| 582 | false, | |
| 583 | &stat_info, | |
| 584 | 0, | |
| 585 | ); | |
| 586 | if (rc != 0) { | |
| 587 | switch (os.errno(rc)) { | |
| 588 | .SUCCESS => {}, | |
| 589 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 590 | .FAULT => unreachable, | |
| 591 | .NOTDIR => unreachable, | |
| 592 | .INVAL => unreachable, | |
| 593 | else => |err| return os.unexpectedErrno(err), | |
| 594 | } | |
| 595 | } | |
| 596 | const statmode = stat_info.mode & os.S.IFMT; | |
| 597 | ||
| 598 | const entry_kind: Entry.Kind = switch (statmode) { | |
| 599 | os.S.IFDIR => .directory, | |
| 600 | os.S.IFBLK => .block_device, | |
| 601 | os.S.IFCHR => .character_device, | |
| 602 | os.S.IFLNK => .sym_link, | |
| 603 | os.S.IFREG => .file, | |
| 604 | os.S.IFIFO => .named_pipe, | |
| 605 | else => .unknown, | |
| 606 | }; | |
| 607 | ||
| 608 | return Entry{ | |
| 609 | .name = name, | |
| 610 | .kind = entry_kind, | |
| 611 | }; | |
| 612 | } | |
| 613 | } | |
| 614 | ||
| 615 | pub fn reset(self: *Self) void { | |
| 616 | self.index = 0; | |
| 617 | self.end_index = 0; | |
| 618 | self.first_iter = true; | |
| 619 | } | |
| 620 | }, | |
| 621 | .linux => struct { | |
| 622 | dir: Dir, | |
| 623 | // The if guard is solely there to prevent compile errors from missing `linux.dirent64` | |
| 624 | // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. | |
| 625 | buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), | |
| 626 | index: usize, | |
| 627 | end_index: usize, | |
| 628 | first_iter: bool, | |
| 629 | ||
| 630 | const Self = @This(); | |
| 631 | const linux = os.linux; | |
| 632 | ||
| 633 | pub const Error = IteratorError; | |
| 634 | ||
| 635 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 636 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 637 | pub fn next(self: *Self) Error!?Entry { | |
| 638 | return self.nextLinux() catch |err| switch (err) { | |
| 639 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 640 | // This matches the behavior of non-Linux UNIX platforms. | |
| 641 | error.DirNotFound => null, | |
| 642 | else => |e| return e, | |
| 643 | }; | |
| 644 | } | |
| 645 | ||
| 646 | pub const ErrorLinux = error{DirNotFound} || IteratorError; | |
| 647 | ||
| 648 | /// Implementation of `next` that can return `error.DirNotFound` if the directory being | |
| 649 | /// iterated was deleted during iteration (this error is Linux specific). | |
| 650 | pub fn nextLinux(self: *Self) ErrorLinux!?Entry { | |
| 651 | start_over: while (true) { | |
| 652 | if (self.index >= self.end_index) { | |
| 653 | if (self.first_iter) { | |
| 654 | std.os.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 655 | self.first_iter = false; | |
| 656 | } | |
| 657 | const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); | |
| 658 | switch (linux.getErrno(rc)) { | |
| 659 | .SUCCESS => {}, | |
| 660 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 661 | .FAULT => unreachable, | |
| 662 | .NOTDIR => unreachable, | |
| 663 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 664 | .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. | |
| 665 | .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. | |
| 666 | else => |err| return os.unexpectedErrno(err), | |
| 667 | } | |
| 668 | if (rc == 0) return null; | |
| 669 | self.index = 0; | |
| 670 | self.end_index = rc; | |
| 671 | } | |
| 672 | const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); | |
| 673 | const next_index = self.index + linux_entry.reclen(); | |
| 674 | self.index = next_index; | |
| 675 | ||
| 676 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); | |
| 677 | ||
| 678 | // skip . and .. entries | |
| 679 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 680 | continue :start_over; | |
| 681 | } | |
| 682 | ||
| 683 | const entry_kind: Entry.Kind = switch (linux_entry.d_type) { | |
| 684 | linux.DT.BLK => .block_device, | |
| 685 | linux.DT.CHR => .character_device, | |
| 686 | linux.DT.DIR => .directory, | |
| 687 | linux.DT.FIFO => .named_pipe, | |
| 688 | linux.DT.LNK => .sym_link, | |
| 689 | linux.DT.REG => .file, | |
| 690 | linux.DT.SOCK => .unix_domain_socket, | |
| 691 | else => .unknown, | |
| 692 | }; | |
| 693 | return Entry{ | |
| 694 | .name = name, | |
| 695 | .kind = entry_kind, | |
| 696 | }; | |
| 697 | } | |
| 698 | } | |
| 699 | ||
| 700 | pub fn reset(self: *Self) void { | |
| 701 | self.index = 0; | |
| 702 | self.end_index = 0; | |
| 703 | self.first_iter = true; | |
| 704 | } | |
| 705 | }, | |
| 706 | .windows => struct { | |
| 707 | dir: Dir, | |
| 708 | buf: [1024]u8 align(@alignOf(os.windows.FILE_BOTH_DIR_INFORMATION)), | |
| 709 | index: usize, | |
| 710 | end_index: usize, | |
| 711 | first_iter: bool, | |
| 712 | name_data: [MAX_NAME_BYTES]u8, | |
| 713 | ||
| 714 | const Self = @This(); | |
| 715 | ||
| 716 | pub const Error = IteratorError; | |
| 717 | ||
| 718 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 719 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 720 | pub fn next(self: *Self) Error!?Entry { | |
| 721 | while (true) { | |
| 722 | const w = os.windows; | |
| 723 | if (self.index >= self.end_index) { | |
| 724 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 725 | const rc = w.ntdll.NtQueryDirectoryFile( | |
| 726 | self.dir.fd, | |
| 727 | null, | |
| 728 | null, | |
| 729 | null, | |
| 730 | &io, | |
| 731 | &self.buf, | |
| 732 | self.buf.len, | |
| 733 | .FileBothDirectoryInformation, | |
| 734 | w.FALSE, | |
| 735 | null, | |
| 736 | if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), | |
| 737 | ); | |
| 738 | self.first_iter = false; | |
| 739 | if (io.Information == 0) return null; | |
| 740 | self.index = 0; | |
| 741 | self.end_index = io.Information; | |
| 742 | switch (rc) { | |
| 743 | .SUCCESS => {}, | |
| 744 | .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability | |
| 745 | ||
| 746 | else => return w.unexpectedStatus(rc), | |
| 747 | } | |
| 748 | } | |
| 749 | ||
| 750 | // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly | |
| 751 | // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) | |
| 752 | const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); | |
| 753 | if (dir_info.NextEntryOffset != 0) { | |
| 754 | self.index += dir_info.NextEntryOffset; | |
| 755 | } else { | |
| 756 | self.index = self.buf.len; | |
| 757 | } | |
| 758 | ||
| 759 | const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; | |
| 760 | ||
| 761 | if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) | |
| 762 | continue; | |
| 763 | // Trust that Windows gives us valid UTF-16LE | |
| 764 | const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; | |
| 765 | const name_utf8 = self.name_data[0..name_utf8_len]; | |
| 766 | const kind: Entry.Kind = blk: { | |
| 767 | const attrs = dir_info.FileAttributes; | |
| 768 | if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; | |
| 769 | if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; | |
| 770 | break :blk .file; | |
| 771 | }; | |
| 772 | return Entry{ | |
| 773 | .name = name_utf8, | |
| 774 | .kind = kind, | |
| 775 | }; | |
| 776 | } | |
| 777 | } | |
| 778 | ||
| 779 | pub fn reset(self: *Self) void { | |
| 780 | self.index = 0; | |
| 781 | self.end_index = 0; | |
| 782 | self.first_iter = true; | |
| 783 | } | |
| 784 | }, | |
| 785 | .wasi => struct { | |
| 786 | dir: Dir, | |
| 787 | buf: [1024]u8, // TODO align(@alignOf(os.wasi.dirent_t)), | |
| 788 | cookie: u64, | |
| 789 | index: usize, | |
| 790 | end_index: usize, | |
| 791 | ||
| 792 | const Self = @This(); | |
| 793 | ||
| 794 | pub const Error = IteratorError; | |
| 795 | ||
| 796 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 797 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 798 | pub fn next(self: *Self) Error!?Entry { | |
| 799 | return self.nextWasi() catch |err| switch (err) { | |
| 800 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 801 | // This matches the behavior of non-Linux UNIX platforms. | |
| 802 | error.DirNotFound => null, | |
| 803 | else => |e| return e, | |
| 804 | }; | |
| 805 | } | |
| 806 | ||
| 807 | pub const ErrorWasi = error{DirNotFound} || IteratorError; | |
| 808 | ||
| 809 | /// Implementation of `next` that can return platform-dependent errors depending on the host platform. | |
| 810 | /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being | |
| 811 | /// iterated was deleted during iteration. | |
| 812 | pub fn nextWasi(self: *Self) ErrorWasi!?Entry { | |
| 813 | // We intentinally use fd_readdir even when linked with libc, | |
| 814 | // since its implementation is exactly the same as below, | |
| 815 | // and we avoid the code complexity here. | |
| 816 | const w = os.wasi; | |
| 817 | start_over: while (true) { | |
| 818 | // According to the WASI spec, the last entry might be truncated, | |
| 819 | // so we need to check if the left buffer contains the whole dirent. | |
| 820 | if (self.end_index - self.index < @sizeOf(w.dirent_t)) { | |
| 821 | var bufused: usize = undefined; | |
| 822 | switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { | |
| 823 | .SUCCESS => {}, | |
| 824 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 825 | .FAULT => unreachable, | |
| 826 | .NOTDIR => unreachable, | |
| 827 | .INVAL => unreachable, | |
| 828 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 829 | .NOTCAPABLE => return error.AccessDenied, | |
| 830 | else => |err| return os.unexpectedErrno(err), | |
| 831 | } | |
| 832 | if (bufused == 0) return null; | |
| 833 | self.index = 0; | |
| 834 | self.end_index = bufused; | |
| 835 | } | |
| 836 | const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); | |
| 837 | const entry_size = @sizeOf(w.dirent_t); | |
| 838 | const name_index = self.index + entry_size; | |
| 839 | if (name_index + entry.d_namlen > self.end_index) { | |
| 840 | // This case, the name is truncated, so we need to call readdir to store the entire name. | |
| 841 | self.end_index = self.index; // Force fd_readdir in the next loop. | |
| 842 | continue :start_over; | |
| 843 | } | |
| 844 | const name = self.buf[name_index .. name_index + entry.d_namlen]; | |
| 845 | ||
| 846 | const next_index = name_index + entry.d_namlen; | |
| 847 | self.index = next_index; | |
| 848 | self.cookie = entry.d_next; | |
| 849 | ||
| 850 | // skip . and .. entries | |
| 851 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 852 | continue :start_over; | |
| 853 | } | |
| 854 | ||
| 855 | const entry_kind: Entry.Kind = switch (entry.d_type) { | |
| 856 | .BLOCK_DEVICE => .block_device, | |
| 857 | .CHARACTER_DEVICE => .character_device, | |
| 858 | .DIRECTORY => .directory, | |
| 859 | .SYMBOLIC_LINK => .sym_link, | |
| 860 | .REGULAR_FILE => .file, | |
| 861 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 862 | else => .unknown, | |
| 863 | }; | |
| 864 | return Entry{ | |
| 865 | .name = name, | |
| 866 | .kind = entry_kind, | |
| 867 | }; | |
| 868 | } | |
| 869 | } | |
| 870 | ||
| 871 | pub fn reset(self: *Self) void { | |
| 872 | self.index = 0; | |
| 873 | self.end_index = 0; | |
| 874 | self.cookie = os.wasi.DIRCOOKIE_START; | |
| 875 | } | |
| 876 | }, | |
| 877 | else => @compileError("unimplemented"), | |
| 878 | }; | |
| 879 | ||
| 880 | pub fn iterate(self: Dir) Iterator { | |
| 881 | return self.iterateImpl(true); | |
| 882 | } | |
| 883 | ||
| 884 | /// Like `iterate`, but will not reset the directory cursor before the first | |
| 885 | /// iteration. This should only be used in cases where it is known that the | |
| 886 | /// `Dir` has not had its cursor modified yet (e.g. it was just opened). | |
| 887 | pub fn iterateAssumeFirstIteration(self: Dir) Iterator { | |
| 888 | return self.iterateImpl(false); | |
| 889 | } | |
| 890 | ||
| 891 | fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { | |
| 892 | switch (builtin.os.tag) { | |
| 893 | .macos, | |
| 894 | .ios, | |
| 895 | .freebsd, | |
| 896 | .netbsd, | |
| 897 | .dragonfly, | |
| 898 | .openbsd, | |
| 899 | .solaris, | |
| 900 | .illumos, | |
| 901 | => return Iterator{ | |
| 902 | .dir = self, | |
| 903 | .seek = 0, | |
| 904 | .index = 0, | |
| 905 | .end_index = 0, | |
| 906 | .buf = undefined, | |
| 907 | .first_iter = first_iter_start_value, | |
| 908 | }, | |
| 909 | .linux, .haiku => return Iterator{ | |
| 910 | .dir = self, | |
| 911 | .index = 0, | |
| 912 | .end_index = 0, | |
| 913 | .buf = undefined, | |
| 914 | .first_iter = first_iter_start_value, | |
| 915 | }, | |
| 916 | .windows => return Iterator{ | |
| 917 | .dir = self, | |
| 918 | .index = 0, | |
| 919 | .end_index = 0, | |
| 920 | .first_iter = first_iter_start_value, | |
| 921 | .buf = undefined, | |
| 922 | .name_data = undefined, | |
| 923 | }, | |
| 924 | .wasi => return Iterator{ | |
| 925 | .dir = self, | |
| 926 | .cookie = os.wasi.DIRCOOKIE_START, | |
| 927 | .index = 0, | |
| 928 | .end_index = 0, | |
| 929 | .buf = undefined, | |
| 930 | }, | |
| 931 | else => @compileError("unimplemented"), | |
| 932 | } | |
| 933 | } | |
| 934 | ||
| 935 | pub const Walker = struct { | |
| 936 | stack: std.ArrayList(StackItem), | |
| 937 | name_buffer: std.ArrayList(u8), | |
| 938 | ||
| 939 | pub const WalkerEntry = struct { | |
| 940 | /// The containing directory. This can be used to operate directly on `basename` | |
| 941 | /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. | |
| 942 | /// The directory remains open until `next` or `deinit` is called. | |
| 943 | dir: Dir, | |
| 944 | basename: []const u8, | |
| 945 | path: []const u8, | |
| 946 | kind: Dir.Entry.Kind, | |
| 947 | }; | |
| 948 | ||
| 949 | const StackItem = struct { | |
| 950 | iter: Dir.Iterator, | |
| 951 | dirname_len: usize, | |
| 952 | }; | |
| 953 | ||
| 954 | /// After each call to this function, and on deinit(), the memory returned | |
| 955 | /// from this function becomes invalid. A copy must be made in order to keep | |
| 956 | /// a reference to the path. | |
| 957 | pub fn next(self: *Walker) !?WalkerEntry { | |
| 958 | while (self.stack.items.len != 0) { | |
| 959 | // `top` and `containing` become invalid after appending to `self.stack` | |
| 960 | var top = &self.stack.items[self.stack.items.len - 1]; | |
| 961 | var containing = top; | |
| 962 | var dirname_len = top.dirname_len; | |
| 963 | if (top.iter.next() catch |err| { | |
| 964 | // If we get an error, then we want the user to be able to continue | |
| 965 | // walking if they want, which means that we need to pop the directory | |
| 966 | // that errored from the stack. Otherwise, all future `next` calls would | |
| 967 | // likely just fail with the same error. | |
| 968 | var item = self.stack.pop(); | |
| 969 | if (self.stack.items.len != 0) { | |
| 970 | item.iter.dir.close(); | |
| 971 | } | |
| 972 | return err; | |
| 973 | }) |base| { | |
| 974 | self.name_buffer.shrinkRetainingCapacity(dirname_len); | |
| 975 | if (self.name_buffer.items.len != 0) { | |
| 976 | try self.name_buffer.append(path.sep); | |
| 977 | dirname_len += 1; | |
| 978 | } | |
| 979 | try self.name_buffer.appendSlice(base.name); | |
| 980 | if (base.kind == .directory) { | |
| 981 | var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { | |
| 982 | error.NameTooLong => unreachable, // no path sep in base.name | |
| 983 | else => |e| return e, | |
| 984 | }; | |
| 985 | { | |
| 986 | errdefer new_dir.close(); | |
| 987 | try self.stack.append(StackItem{ | |
| 988 | .iter = new_dir.iterateAssumeFirstIteration(), | |
| 989 | .dirname_len = self.name_buffer.items.len, | |
| 990 | }); | |
| 991 | top = &self.stack.items[self.stack.items.len - 1]; | |
| 992 | containing = &self.stack.items[self.stack.items.len - 2]; | |
| 993 | } | |
| 994 | } | |
| 995 | return WalkerEntry{ | |
| 996 | .dir = containing.iter.dir, | |
| 997 | .basename = self.name_buffer.items[dirname_len..], | |
| 998 | .path = self.name_buffer.items, | |
| 999 | .kind = base.kind, | |
| 1000 | }; | |
| 1001 | } else { | |
| 1002 | var item = self.stack.pop(); | |
| 1003 | if (self.stack.items.len != 0) { | |
| 1004 | item.iter.dir.close(); | |
| 1005 | } | |
| 1006 | } | |
| 1007 | } | |
| 1008 | return null; | |
| 1009 | } | |
| 1010 | ||
| 1011 | pub fn deinit(self: *Walker) void { | |
| 1012 | // Close any remaining directories except the initial one (which is always at index 0) | |
| 1013 | if (self.stack.items.len > 1) { | |
| 1014 | for (self.stack.items[1..]) |*item| { | |
| 1015 | item.iter.dir.close(); | |
| 1016 | } | |
| 1017 | } | |
| 1018 | self.stack.deinit(); | |
| 1019 | self.name_buffer.deinit(); | |
| 1020 | } | |
| 1021 | }; | |
| 1022 | ||
| 1023 | /// Recursively iterates over a directory. | |
| 1024 | /// `self` must have been opened with `OpenDirOptions{.iterate = true}`. | |
| 1025 | /// Must call `Walker.deinit` when done. | |
| 1026 | /// The order of returned file system entries is undefined. | |
| 1027 | /// `self` will not be closed after walking it. | |
| 1028 | pub fn walk(self: Dir, allocator: Allocator) !Walker { | |
| 1029 | var name_buffer = std.ArrayList(u8).init(allocator); | |
| 1030 | errdefer name_buffer.deinit(); | |
| 1031 | ||
| 1032 | var stack = std.ArrayList(Walker.StackItem).init(allocator); | |
| 1033 | errdefer stack.deinit(); | |
| 1034 | ||
| 1035 | try stack.append(Walker.StackItem{ | |
| 1036 | .iter = self.iterate(), | |
| 1037 | .dirname_len = 0, | |
| 1038 | }); | |
| 1039 | ||
| 1040 | return Walker{ | |
| 1041 | .stack = stack, | |
| 1042 | .name_buffer = name_buffer, | |
| 1043 | }; | |
| 1044 | } | |
| 1045 | ||
| 1046 | pub const OpenError = error{ | |
| 1047 | FileNotFound, | |
| 1048 | NotDir, | |
| 1049 | InvalidHandle, | |
| 1050 | AccessDenied, | |
| 1051 | SymLinkLoop, | |
| 1052 | ProcessFdQuotaExceeded, | |
| 1053 | NameTooLong, | |
| 1054 | SystemFdQuotaExceeded, | |
| 1055 | NoDevice, | |
| 1056 | SystemResources, | |
| 1057 | InvalidUtf8, | |
| 1058 | BadPathName, | |
| 1059 | DeviceBusy, | |
| 1060 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1061 | NetworkNotFound, | |
| 1062 | } || os.UnexpectedError; | |
| 1063 | ||
| 1064 | pub fn close(self: *Dir) void { | |
| 1065 | if (need_async_thread) { | |
| 1066 | std.event.Loop.instance.?.close(self.fd); | |
| 1067 | } else { | |
| 1068 | os.close(self.fd); | |
| 1069 | } | |
| 1070 | self.* = undefined; | |
| 1071 | } | |
| 1072 | ||
| 1073 | /// Opens a file for reading or writing, without attempting to create a new file. | |
| 1074 | /// To create a new file, see `createFile`. | |
| 1075 | /// Call `File.close` to release the resource. | |
| 1076 | /// Asserts that the path parameter has no null bytes. | |
| 1077 | pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1078 | if (builtin.os.tag == .windows) { | |
| 1079 | const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1080 | return self.openFileW(path_w.span(), flags); | |
| 1081 | } | |
| 1082 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1083 | return self.openFileWasi(sub_path, flags); | |
| 1084 | } | |
| 1085 | const path_c = try os.toPosixPath(sub_path); | |
| 1086 | return self.openFileZ(&path_c, flags); | |
| 1087 | } | |
| 1088 | ||
| 1089 | /// Same as `openFile` but WASI only. | |
| 1090 | pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1091 | const w = os.wasi; | |
| 1092 | var fdflags: w.fdflags_t = 0x0; | |
| 1093 | var base: w.rights_t = 0x0; | |
| 1094 | if (flags.isRead()) { | |
| 1095 | base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; | |
| 1096 | } | |
| 1097 | if (flags.isWrite()) { | |
| 1098 | fdflags |= w.FDFLAG.APPEND; | |
| 1099 | base |= w.RIGHT.FD_WRITE | | |
| 1100 | w.RIGHT.FD_TELL | | |
| 1101 | w.RIGHT.FD_SEEK | | |
| 1102 | w.RIGHT.FD_DATASYNC | | |
| 1103 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 1104 | w.RIGHT.FD_SYNC | | |
| 1105 | w.RIGHT.FD_ALLOCATE | | |
| 1106 | w.RIGHT.FD_ADVISE | | |
| 1107 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 1108 | w.RIGHT.FD_FILESTAT_SET_SIZE; | |
| 1109 | } | |
| 1110 | const fd = try os.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); | |
| 1111 | return File{ .handle = fd }; | |
| 1112 | } | |
| 1113 | ||
| 1114 | /// Same as `openFile` but the path parameter is null-terminated. | |
| 1115 | pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 1116 | if (builtin.os.tag == .windows) { | |
| 1117 | const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path); | |
| 1118 | return self.openFileW(path_w.span(), flags); | |
| 1119 | } | |
| 1120 | ||
| 1121 | var os_flags: u32 = 0; | |
| 1122 | if (@hasDecl(os.O, "CLOEXEC")) os_flags = os.O.CLOEXEC; | |
| 1123 | ||
| 1124 | // Use the O locking flags if the os supports them to acquire the lock | |
| 1125 | // atomically. | |
| 1126 | const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); | |
| 1127 | if (has_flock_open_flags) { | |
| 1128 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 1129 | // is successful. | |
| 1130 | const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) | |
| 1131 | os.O.NONBLOCK | |
| 1132 | else | |
| 1133 | 0; | |
| 1134 | os_flags |= switch (flags.lock) { | |
| 1135 | .none => @as(u32, 0), | |
| 1136 | .shared => os.O.SHLOCK | nonblocking_lock_flag, | |
| 1137 | .exclusive => os.O.EXLOCK | nonblocking_lock_flag, | |
| 1138 | }; | |
| 1139 | } | |
| 1140 | if (@hasDecl(os.O, "LARGEFILE")) { | |
| 1141 | os_flags |= os.O.LARGEFILE; | |
| 1142 | } | |
| 1143 | if (@hasDecl(os.O, "NOCTTY") and !flags.allow_ctty) { | |
| 1144 | os_flags |= os.O.NOCTTY; | |
| 1145 | } | |
| 1146 | os_flags |= switch (flags.mode) { | |
| 1147 | .read_only => @as(u32, os.O.RDONLY), | |
| 1148 | .write_only => @as(u32, os.O.WRONLY), | |
| 1149 | .read_write => @as(u32, os.O.RDWR), | |
| 1150 | }; | |
| 1151 | const fd = if (flags.intended_io_mode != .blocking) | |
| 1152 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) | |
| 1153 | else | |
| 1154 | try os.openatZ(self.fd, sub_path, os_flags, 0); | |
| 1155 | errdefer os.close(fd); | |
| 1156 | ||
| 1157 | // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block | |
| 1158 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1159 | if (@hasDecl(os.system, "LOCK") and builtin.target.os.tag != .wasi) { | |
| 1160 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1161 | // TODO: integrate async I/O | |
| 1162 | const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); | |
| 1163 | try os.flock(fd, switch (flags.lock) { | |
| 1164 | .none => unreachable, | |
| 1165 | .shared => os.LOCK.SH | lock_nonblocking, | |
| 1166 | .exclusive => os.LOCK.EX | lock_nonblocking, | |
| 1167 | }); | |
| 1168 | } | |
| 1169 | } | |
| 1170 | ||
| 1171 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1172 | var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { | |
| 1173 | error.FileBusy => unreachable, | |
| 1174 | error.Locked => unreachable, | |
| 1175 | error.PermissionDenied => unreachable, | |
| 1176 | error.DeadLock => unreachable, | |
| 1177 | error.LockedRegionLimitExceeded => unreachable, | |
| 1178 | else => |e| return e, | |
| 1179 | }; | |
| 1180 | fl_flags &= ~@as(usize, os.O.NONBLOCK); | |
| 1181 | _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1182 | error.FileBusy => unreachable, | |
| 1183 | error.Locked => unreachable, | |
| 1184 | error.PermissionDenied => unreachable, | |
| 1185 | error.DeadLock => unreachable, | |
| 1186 | error.LockedRegionLimitExceeded => unreachable, | |
| 1187 | else => |e| return e, | |
| 1188 | }; | |
| 1189 | } | |
| 1190 | ||
| 1191 | return File{ | |
| 1192 | .handle = fd, | |
| 1193 | .capable_io_mode = .blocking, | |
| 1194 | .intended_io_mode = flags.intended_io_mode, | |
| 1195 | }; | |
| 1196 | } | |
| 1197 | ||
| 1198 | /// Same as `openFile` but Windows-only and the path parameter is | |
| 1199 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1200 | pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { | |
| 1201 | const w = os.windows; | |
| 1202 | const file: File = .{ | |
| 1203 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 1204 | .dir = self.fd, | |
| 1205 | .access_mask = w.SYNCHRONIZE | | |
| 1206 | (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | | |
| 1207 | (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), | |
| 1208 | .creation = w.FILE_OPEN, | |
| 1209 | .io_mode = flags.intended_io_mode, | |
| 1210 | }), | |
| 1211 | .capable_io_mode = std.io.default_mode, | |
| 1212 | .intended_io_mode = flags.intended_io_mode, | |
| 1213 | }; | |
| 1214 | errdefer file.close(); | |
| 1215 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1216 | const range_off: w.LARGE_INTEGER = 0; | |
| 1217 | const range_len: w.LARGE_INTEGER = 1; | |
| 1218 | const exclusive = switch (flags.lock) { | |
| 1219 | .none => return file, | |
| 1220 | .shared => false, | |
| 1221 | .exclusive => true, | |
| 1222 | }; | |
| 1223 | try w.LockFile( | |
| 1224 | file.handle, | |
| 1225 | null, | |
| 1226 | null, | |
| 1227 | null, | |
| 1228 | &io, | |
| 1229 | &range_off, | |
| 1230 | &range_len, | |
| 1231 | null, | |
| 1232 | @intFromBool(flags.lock_nonblocking), | |
| 1233 | @intFromBool(exclusive), | |
| 1234 | ); | |
| 1235 | return file; | |
| 1236 | } | |
| 1237 | ||
| 1238 | /// Creates, opens, or overwrites a file with write access. | |
| 1239 | /// Call `File.close` on the result when done. | |
| 1240 | /// Asserts that the path parameter has no null bytes. | |
| 1241 | pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1242 | if (builtin.os.tag == .windows) { | |
| 1243 | const path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1244 | return self.createFileW(path_w.span(), flags); | |
| 1245 | } | |
| 1246 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1247 | return self.createFileWasi(sub_path, flags); | |
| 1248 | } | |
| 1249 | const path_c = try os.toPosixPath(sub_path); | |
| 1250 | return self.createFileZ(&path_c, flags); | |
| 1251 | } | |
| 1252 | ||
| 1253 | /// Same as `createFile` but WASI only. | |
| 1254 | pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1255 | const w = os.wasi; | |
| 1256 | var oflags = w.O.CREAT; | |
| 1257 | var base: w.rights_t = w.RIGHT.FD_WRITE | | |
| 1258 | w.RIGHT.FD_DATASYNC | | |
| 1259 | w.RIGHT.FD_SEEK | | |
| 1260 | w.RIGHT.FD_TELL | | |
| 1261 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 1262 | w.RIGHT.FD_SYNC | | |
| 1263 | w.RIGHT.FD_ALLOCATE | | |
| 1264 | w.RIGHT.FD_ADVISE | | |
| 1265 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 1266 | w.RIGHT.FD_FILESTAT_SET_SIZE | | |
| 1267 | w.RIGHT.FD_FILESTAT_GET; | |
| 1268 | if (flags.read) { | |
| 1269 | base |= w.RIGHT.FD_READ; | |
| 1270 | } | |
| 1271 | if (flags.truncate) { | |
| 1272 | oflags |= w.O.TRUNC; | |
| 1273 | } | |
| 1274 | if (flags.exclusive) { | |
| 1275 | oflags |= w.O.EXCL; | |
| 1276 | } | |
| 1277 | const fd = try os.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); | |
| 1278 | return File{ .handle = fd }; | |
| 1279 | } | |
| 1280 | ||
| 1281 | /// Same as `createFile` but the path parameter is null-terminated. | |
| 1282 | pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 1283 | if (builtin.os.tag == .windows) { | |
| 1284 | const path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1285 | return self.createFileW(path_w.span(), flags); | |
| 1286 | } | |
| 1287 | ||
| 1288 | // Use the O locking flags if the os supports them to acquire the lock | |
| 1289 | // atomically. | |
| 1290 | const has_flock_open_flags = @hasDecl(os.O, "EXLOCK"); | |
| 1291 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 1292 | // is successful. | |
| 1293 | const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) | |
| 1294 | os.O.NONBLOCK | |
| 1295 | else | |
| 1296 | 0; | |
| 1297 | const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { | |
| 1298 | .none => @as(u32, 0), | |
| 1299 | .shared => os.O.SHLOCK | nonblocking_lock_flag, | |
| 1300 | .exclusive => os.O.EXLOCK | nonblocking_lock_flag, | |
| 1301 | } else 0; | |
| 1302 | ||
| 1303 | const O_LARGEFILE = if (@hasDecl(os.O, "LARGEFILE")) os.O.LARGEFILE else 0; | |
| 1304 | const os_flags = lock_flag | O_LARGEFILE | os.O.CREAT | os.O.CLOEXEC | | |
| 1305 | (if (flags.truncate) @as(u32, os.O.TRUNC) else 0) | | |
| 1306 | (if (flags.read) @as(u32, os.O.RDWR) else os.O.WRONLY) | | |
| 1307 | (if (flags.exclusive) @as(u32, os.O.EXCL) else 0); | |
| 1308 | const fd = if (flags.intended_io_mode != .blocking) | |
| 1309 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) | |
| 1310 | else | |
| 1311 | try os.openatZ(self.fd, sub_path_c, os_flags, flags.mode); | |
| 1312 | errdefer os.close(fd); | |
| 1313 | ||
| 1314 | // WASI doesn't have os.flock so we intetinally check OS prior to the inner if block | |
| 1315 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1316 | if (builtin.target.os.tag != .wasi) { | |
| 1317 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1318 | // TODO: integrate async I/O | |
| 1319 | const lock_nonblocking = if (flags.lock_nonblocking) os.LOCK.NB else @as(i32, 0); | |
| 1320 | try os.flock(fd, switch (flags.lock) { | |
| 1321 | .none => unreachable, | |
| 1322 | .shared => os.LOCK.SH | lock_nonblocking, | |
| 1323 | .exclusive => os.LOCK.EX | lock_nonblocking, | |
| 1324 | }); | |
| 1325 | } | |
| 1326 | } | |
| 1327 | ||
| 1328 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1329 | var fl_flags = os.fcntl(fd, os.F.GETFL, 0) catch |err| switch (err) { | |
| 1330 | error.FileBusy => unreachable, | |
| 1331 | error.Locked => unreachable, | |
| 1332 | error.PermissionDenied => unreachable, | |
| 1333 | error.DeadLock => unreachable, | |
| 1334 | error.LockedRegionLimitExceeded => unreachable, | |
| 1335 | else => |e| return e, | |
| 1336 | }; | |
| 1337 | fl_flags &= ~@as(usize, os.O.NONBLOCK); | |
| 1338 | _ = os.fcntl(fd, os.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1339 | error.FileBusy => unreachable, | |
| 1340 | error.Locked => unreachable, | |
| 1341 | error.PermissionDenied => unreachable, | |
| 1342 | error.DeadLock => unreachable, | |
| 1343 | error.LockedRegionLimitExceeded => unreachable, | |
| 1344 | else => |e| return e, | |
| 1345 | }; | |
| 1346 | } | |
| 1347 | ||
| 1348 | return File{ | |
| 1349 | .handle = fd, | |
| 1350 | .capable_io_mode = .blocking, | |
| 1351 | .intended_io_mode = flags.intended_io_mode, | |
| 1352 | }; | |
| 1353 | } | |
| 1354 | ||
| 1355 | /// Same as `createFile` but Windows-only and the path parameter is | |
| 1356 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1357 | pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { | |
| 1358 | const w = os.windows; | |
| 1359 | const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; | |
| 1360 | const file: File = .{ | |
| 1361 | .handle = try os.windows.OpenFile(sub_path_w, .{ | |
| 1362 | .dir = self.fd, | |
| 1363 | .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, | |
| 1364 | .creation = if (flags.exclusive) | |
| 1365 | @as(u32, w.FILE_CREATE) | |
| 1366 | else if (flags.truncate) | |
| 1367 | @as(u32, w.FILE_OVERWRITE_IF) | |
| 1368 | else | |
| 1369 | @as(u32, w.FILE_OPEN_IF), | |
| 1370 | .io_mode = flags.intended_io_mode, | |
| 1371 | }), | |
| 1372 | .capable_io_mode = std.io.default_mode, | |
| 1373 | .intended_io_mode = flags.intended_io_mode, | |
| 1374 | }; | |
| 1375 | errdefer file.close(); | |
| 1376 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1377 | const range_off: w.LARGE_INTEGER = 0; | |
| 1378 | const range_len: w.LARGE_INTEGER = 1; | |
| 1379 | const exclusive = switch (flags.lock) { | |
| 1380 | .none => return file, | |
| 1381 | .shared => false, | |
| 1382 | .exclusive => true, | |
| 1383 | }; | |
| 1384 | try w.LockFile( | |
| 1385 | file.handle, | |
| 1386 | null, | |
| 1387 | null, | |
| 1388 | null, | |
| 1389 | &io, | |
| 1390 | &range_off, | |
| 1391 | &range_len, | |
| 1392 | null, | |
| 1393 | @intFromBool(flags.lock_nonblocking), | |
| 1394 | @intFromBool(exclusive), | |
| 1395 | ); | |
| 1396 | return file; | |
| 1397 | } | |
| 1398 | ||
| 1399 | /// Creates a single directory with a relative or absolute path. | |
| 1400 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1401 | /// To operate on only absolute paths, see `makeDirAbsolute`. | |
| 1402 | pub fn makeDir(self: Dir, sub_path: []const u8) !void { | |
| 1403 | try os.mkdirat(self.fd, sub_path, default_new_dir_mode); | |
| 1404 | } | |
| 1405 | ||
| 1406 | /// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. | |
| 1407 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1408 | /// To operate on only absolute paths, see `makeDirAbsoluteZ`. | |
| 1409 | pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { | |
| 1410 | try os.mkdiratZ(self.fd, sub_path, default_new_dir_mode); | |
| 1411 | } | |
| 1412 | ||
| 1413 | /// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. | |
| 1414 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1415 | /// To operate on only absolute paths, see `makeDirAbsoluteW`. | |
| 1416 | pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { | |
| 1417 | try os.mkdiratW(self.fd, sub_path, default_new_dir_mode); | |
| 1418 | } | |
| 1419 | ||
| 1420 | /// Calls makeDir iteratively to make an entire path | |
| 1421 | /// (i.e. creating any parent directories that do not exist). | |
| 1422 | /// Returns success if the path already exists and is a directory. | |
| 1423 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1424 | /// have been modified regardless. | |
| 1425 | pub fn makePath(self: Dir, sub_path: []const u8) !void { | |
| 1426 | var it = try path.componentIterator(sub_path); | |
| 1427 | var component = it.last() orelse return; | |
| 1428 | while (true) { | |
| 1429 | self.makeDir(component.path) catch |err| switch (err) { | |
| 1430 | error.PathAlreadyExists => { | |
| 1431 | // TODO stat the file and return an error if it's not a directory | |
| 1432 | // this is important because otherwise a dangling symlink | |
| 1433 | // could cause an infinite loop | |
| 1434 | }, | |
| 1435 | error.FileNotFound => |e| { | |
| 1436 | component = it.previous() orelse return e; | |
| 1437 | continue; | |
| 1438 | }, | |
| 1439 | else => |e| return e, | |
| 1440 | }; | |
| 1441 | component = it.next() orelse return; | |
| 1442 | } | |
| 1443 | } | |
| 1444 | ||
| 1445 | /// Calls makeOpenDirAccessMaskW iteratively to make an entire path | |
| 1446 | /// (i.e. creating any parent directories that do not exist). | |
| 1447 | /// Opens the dir if the path already exists and is a directory. | |
| 1448 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1449 | /// have been modified regardless. | |
| 1450 | fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { | |
| 1451 | const w = os.windows; | |
| 1452 | var it = try path.componentIterator(sub_path); | |
| 1453 | // If there are no components in the path, then create a dummy component with the full path. | |
| 1454 | var component = it.last() orelse path.NativeUtf8ComponentIterator.Component{ | |
| 1455 | .name = "", | |
| 1456 | .path = sub_path, | |
| 1457 | }; | |
| 1458 | ||
| 1459 | while (true) { | |
| 1460 | const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); | |
| 1461 | const is_last = it.peekNext() == null; | |
| 1462 | var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ | |
| 1463 | .no_follow = no_follow, | |
| 1464 | .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, | |
| 1465 | }) catch |err| switch (err) { | |
| 1466 | error.FileNotFound => |e| { | |
| 1467 | component = it.previous() orelse return e; | |
| 1468 | continue; | |
| 1469 | }, | |
| 1470 | else => |e| return e, | |
| 1471 | }; | |
| 1472 | ||
| 1473 | component = it.next() orelse return result; | |
| 1474 | // Don't leak the intermediate file handles | |
| 1475 | result.close(); | |
| 1476 | } | |
| 1477 | } | |
| 1478 | ||
| 1479 | /// This function performs `makePath`, followed by `openDir`. | |
| 1480 | /// If supported by the OS, this operation is atomic. It is not atomic on | |
| 1481 | /// all operating systems. | |
| 1482 | /// On Windows, this function performs `makeOpenPathAccessMaskW`. | |
| 1483 | pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { | |
| 1484 | return switch (builtin.os.tag) { | |
| 1485 | .windows => { | |
| 1486 | const w = os.windows; | |
| 1487 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1488 | w.SYNCHRONIZE | w.FILE_TRAVERSE | | |
| 1489 | (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else 0); | |
| 1490 | ||
| 1491 | return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); | |
| 1492 | }, | |
| 1493 | else => { | |
| 1494 | return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { | |
| 1495 | error.FileNotFound => { | |
| 1496 | try self.makePath(sub_path); | |
| 1497 | return self.openDir(sub_path, open_dir_options); | |
| 1498 | }, | |
| 1499 | else => |e| return e, | |
| 1500 | }; | |
| 1501 | }, | |
| 1502 | }; | |
| 1503 | } | |
| 1504 | ||
| 1505 | /// This function returns the canonicalized absolute pathname of | |
| 1506 | /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this | |
| 1507 | /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` | |
| 1508 | /// argument. | |
| 1509 | /// This function is not universally supported by all platforms. | |
| 1510 | /// Currently supported hosts are: Linux, macOS, and Windows. | |
| 1511 | /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. | |
| 1512 | pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { | |
| 1513 | if (builtin.os.tag == .wasi) { | |
| 1514 | @compileError("realpath is not available on WASI"); | |
| 1515 | } | |
| 1516 | if (builtin.os.tag == .windows) { | |
| 1517 | const pathname_w = try os.windows.sliceToPrefixedFileW(self.fd, pathname); | |
| 1518 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1519 | } | |
| 1520 | const pathname_c = try os.toPosixPath(pathname); | |
| 1521 | return self.realpathZ(&pathname_c, out_buffer); | |
| 1522 | } | |
| 1523 | ||
| 1524 | /// Same as `Dir.realpath` except `pathname` is null-terminated. | |
| 1525 | /// See also `Dir.realpath`, `realpathZ`. | |
| 1526 | pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { | |
| 1527 | if (builtin.os.tag == .windows) { | |
| 1528 | const pathname_w = try os.windows.cStrToPrefixedFileW(self.fd, pathname); | |
| 1529 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1530 | } | |
| 1531 | ||
| 1532 | const flags = if (builtin.os.tag == .linux) os.O.PATH | os.O.NONBLOCK | os.O.CLOEXEC else os.O.NONBLOCK | os.O.CLOEXEC; | |
| 1533 | const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { | |
| 1534 | error.FileLocksNotSupported => unreachable, | |
| 1535 | else => |e| return e, | |
| 1536 | }; | |
| 1537 | defer os.close(fd); | |
| 1538 | ||
| 1539 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1540 | // have a variant that takes an arbitrary-size buffer. | |
| 1541 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1542 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1543 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1544 | // anyway. | |
| 1545 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 1546 | const out_path = try os.getFdPath(fd, &buffer); | |
| 1547 | ||
| 1548 | if (out_path.len > out_buffer.len) { | |
| 1549 | return error.NameTooLong; | |
| 1550 | } | |
| 1551 | ||
| 1552 | const result = out_buffer[0..out_path.len]; | |
| 1553 | @memcpy(result, out_path); | |
| 1554 | return result; | |
| 1555 | } | |
| 1556 | ||
| 1557 | /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. | |
| 1558 | /// See also `Dir.realpath`, `realpathW`. | |
| 1559 | pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { | |
| 1560 | const w = os.windows; | |
| 1561 | ||
| 1562 | const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; | |
| 1563 | const share_access = w.FILE_SHARE_READ; | |
| 1564 | const creation = w.FILE_OPEN; | |
| 1565 | const h_file = blk: { | |
| 1566 | const res = w.OpenFile(pathname, .{ | |
| 1567 | .dir = self.fd, | |
| 1568 | .access_mask = access_mask, | |
| 1569 | .share_access = share_access, | |
| 1570 | .creation = creation, | |
| 1571 | .io_mode = .blocking, | |
| 1572 | .filter = .any, | |
| 1573 | }) catch |err| switch (err) { | |
| 1574 | error.WouldBlock => unreachable, | |
| 1575 | else => |e| return e, | |
| 1576 | }; | |
| 1577 | break :blk res; | |
| 1578 | }; | |
| 1579 | defer w.CloseHandle(h_file); | |
| 1580 | ||
| 1581 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1582 | // have a variant that takes an arbitrary-size buffer. | |
| 1583 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1584 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1585 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1586 | // anyway. | |
| 1587 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 1588 | const out_path = try os.getFdPath(h_file, &buffer); | |
| 1589 | ||
| 1590 | if (out_path.len > out_buffer.len) { | |
| 1591 | return error.NameTooLong; | |
| 1592 | } | |
| 1593 | ||
| 1594 | const result = out_buffer[0..out_path.len]; | |
| 1595 | @memcpy(result, out_path); | |
| 1596 | return result; | |
| 1597 | } | |
| 1598 | ||
| 1599 | /// Same as `Dir.realpath` except caller must free the returned memory. | |
| 1600 | /// See also `Dir.realpath`. | |
| 1601 | pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { | |
| 1602 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1603 | // have a variant that takes an arbitrary-size buffer. | |
| 1604 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1605 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1606 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1607 | // anyway. | |
| 1608 | var buf: [MAX_PATH_BYTES]u8 = undefined; | |
| 1609 | return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); | |
| 1610 | } | |
| 1611 | ||
| 1612 | /// Changes the current working directory to the open directory handle. | |
| 1613 | /// This modifies global state and can have surprising effects in multi- | |
| 1614 | /// threaded applications. Most applications and especially libraries should | |
| 1615 | /// not call this function as a general rule, however it can have use cases | |
| 1616 | /// in, for example, implementing a shell, or child process execution. | |
| 1617 | /// Not all targets support this. For example, WASI does not have the concept | |
| 1618 | /// of a current working directory. | |
| 1619 | pub fn setAsCwd(self: Dir) !void { | |
| 1620 | if (builtin.os.tag == .wasi) { | |
| 1621 | @compileError("changing cwd is not currently possible in WASI"); | |
| 1622 | } | |
| 1623 | if (builtin.os.tag == .windows) { | |
| 1624 | var dir_path_buffer: [os.windows.PATH_MAX_WIDE]u16 = undefined; | |
| 1625 | const dir_path = try os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); | |
| 1626 | if (builtin.link_libc) { | |
| 1627 | return os.chdirW(dir_path); | |
| 1628 | } | |
| 1629 | return os.windows.SetCurrentDirectory(dir_path); | |
| 1630 | } | |
| 1631 | try os.fchdir(self.fd); | |
| 1632 | } | |
| 1633 | ||
| 1634 | pub const OpenDirOptions = struct { | |
| 1635 | /// `true` means the opened directory can be used as the `Dir` parameter | |
| 1636 | /// for functions which operate based on an open directory handle. When `false`, | |
| 1637 | /// such operations are Illegal Behavior. | |
| 1638 | access_sub_paths: bool = true, | |
| 1639 | ||
| 1640 | /// `true` means the opened directory can be scanned for the files and sub-directories | |
| 1641 | /// of the result. It means the `iterate` function can be called. | |
| 1642 | iterate: bool = false, | |
| 1643 | ||
| 1644 | /// `true` means it won't dereference the symlinks. | |
| 1645 | no_follow: bool = false, | |
| 1646 | }; | |
| 1647 | ||
| 1648 | /// Opens a directory at the given path. The directory is a system resource that remains | |
| 1649 | /// open until `close` is called on the result. | |
| 1650 | /// The directory cannot be iterated unless the `iterate` option is set to `true`. | |
| 1651 | /// | |
| 1652 | /// Asserts that the path parameter has no null bytes. | |
| 1653 | pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1654 | if (builtin.os.tag == .windows) { | |
| 1655 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1656 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1657 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1658 | return self.openDirWasi(sub_path, args); | |
| 1659 | } else { | |
| 1660 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1661 | return self.openDirZ(&sub_path_c, args); | |
| 1662 | } | |
| 1663 | } | |
| 1664 | ||
| 1665 | /// Same as `openDir` except only WASI. | |
| 1666 | pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1667 | const w = os.wasi; | |
| 1668 | var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; | |
| 1669 | if (args.access_sub_paths) { | |
| 1670 | base |= w.RIGHT.FD_READDIR | | |
| 1671 | w.RIGHT.PATH_CREATE_DIRECTORY | | |
| 1672 | w.RIGHT.PATH_CREATE_FILE | | |
| 1673 | w.RIGHT.PATH_LINK_SOURCE | | |
| 1674 | w.RIGHT.PATH_LINK_TARGET | | |
| 1675 | w.RIGHT.PATH_OPEN | | |
| 1676 | w.RIGHT.PATH_READLINK | | |
| 1677 | w.RIGHT.PATH_RENAME_SOURCE | | |
| 1678 | w.RIGHT.PATH_RENAME_TARGET | | |
| 1679 | w.RIGHT.PATH_FILESTAT_GET | | |
| 1680 | w.RIGHT.PATH_FILESTAT_SET_SIZE | | |
| 1681 | w.RIGHT.PATH_FILESTAT_SET_TIMES | | |
| 1682 | w.RIGHT.PATH_SYMLINK | | |
| 1683 | w.RIGHT.PATH_REMOVE_DIRECTORY | | |
| 1684 | w.RIGHT.PATH_UNLINK_FILE; | |
| 1685 | } | |
| 1686 | const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; | |
| 1687 | // TODO do we really need all the rights here? | |
| 1688 | const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; | |
| 1689 | ||
| 1690 | const result = os.openatWasi( | |
| 1691 | self.fd, | |
| 1692 | sub_path, | |
| 1693 | symlink_flags, | |
| 1694 | w.O.DIRECTORY, | |
| 1695 | 0x0, | |
| 1696 | base, | |
| 1697 | inheriting, | |
| 1698 | ); | |
| 1699 | const fd = result catch |err| switch (err) { | |
| 1700 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1701 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1702 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1703 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1704 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1705 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1706 | error.FileBusy => unreachable, // can't happen for directories | |
| 1707 | else => |e| return e, | |
| 1708 | }; | |
| 1709 | return Dir{ .fd = fd }; | |
| 1710 | } | |
| 1711 | ||
| 1712 | /// Same as `openDir` except the parameter is null-terminated. | |
| 1713 | pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1714 | if (builtin.os.tag == .windows) { | |
| 1715 | const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1716 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1717 | } | |
| 1718 | const symlink_flags: u32 = if (args.no_follow) os.O.NOFOLLOW else 0x0; | |
| 1719 | if (!args.iterate) { | |
| 1720 | const O_PATH = if (@hasDecl(os.O, "PATH")) os.O.PATH else 0; | |
| 1721 | return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | O_PATH | symlink_flags); | |
| 1722 | } else { | |
| 1723 | return self.openDirFlagsZ(sub_path_c, os.O.DIRECTORY | os.O.RDONLY | os.O.CLOEXEC | symlink_flags); | |
| 1724 | } | |
| 1725 | } | |
| 1726 | ||
| 1727 | /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. | |
| 1728 | /// This function asserts the target OS is Windows. | |
| 1729 | pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { | |
| 1730 | const w = os.windows; | |
| 1731 | // TODO remove some of these flags if args.access_sub_paths is false | |
| 1732 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1733 | w.SYNCHRONIZE | w.FILE_TRAVERSE; | |
| 1734 | const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; | |
| 1735 | const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ | |
| 1736 | .no_follow = args.no_follow, | |
| 1737 | .create_disposition = w.FILE_OPEN, | |
| 1738 | }); | |
| 1739 | return dir; | |
| 1740 | } | |
| 1741 | ||
| 1742 | /// `flags` must contain `os.O.DIRECTORY`. | |
| 1743 | fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { | |
| 1744 | const result = if (need_async_thread) | |
| 1745 | std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) | |
| 1746 | else | |
| 1747 | os.openatZ(self.fd, sub_path_c, flags, 0); | |
| 1748 | const fd = result catch |err| switch (err) { | |
| 1749 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1750 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1751 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1752 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1753 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1754 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1755 | error.FileBusy => unreachable, // can't happen for directories | |
| 1756 | else => |e| return e, | |
| 1757 | }; | |
| 1758 | return Dir{ .fd = fd }; | |
| 1759 | } | |
| 1760 | ||
| 1761 | const MakeOpenDirAccessMaskWOptions = struct { | |
| 1762 | no_follow: bool, | |
| 1763 | create_disposition: u32, | |
| 1764 | }; | |
| 1765 | ||
| 1766 | fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { | |
| 1767 | const w = os.windows; | |
| 1768 | ||
| 1769 | var result = Dir{ | |
| 1770 | .fd = undefined, | |
| 1771 | }; | |
| 1772 | ||
| 1773 | const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); | |
| 1774 | var nt_name = w.UNICODE_STRING{ | |
| 1775 | .Length = path_len_bytes, | |
| 1776 | .MaximumLength = path_len_bytes, | |
| 1777 | .Buffer = @constCast(sub_path_w), | |
| 1778 | }; | |
| 1779 | var attr = w.OBJECT_ATTRIBUTES{ | |
| 1780 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | |
| 1781 | .RootDirectory = if (path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, | |
| 1782 | .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. | |
| 1783 | .ObjectName = &nt_name, | |
| 1784 | .SecurityDescriptor = null, | |
| 1785 | .SecurityQualityOfService = null, | |
| 1786 | }; | |
| 1787 | const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; | |
| 1788 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1789 | const rc = w.ntdll.NtCreateFile( | |
| 1790 | &result.fd, | |
| 1791 | access_mask, | |
| 1792 | &attr, | |
| 1793 | &io, | |
| 1794 | null, | |
| 1795 | w.FILE_ATTRIBUTE_NORMAL, | |
| 1796 | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, | |
| 1797 | flags.create_disposition, | |
| 1798 | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, | |
| 1799 | null, | |
| 1800 | 0, | |
| 1801 | ); | |
| 1802 | ||
| 1803 | switch (rc) { | |
| 1804 | .SUCCESS => return result, | |
| 1805 | .OBJECT_NAME_INVALID => return error.BadPathName, | |
| 1806 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | |
| 1807 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | |
| 1808 | .NOT_A_DIRECTORY => return error.NotDir, | |
| 1809 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | |
| 1810 | // and the directory is trying to be opened for iteration. | |
| 1811 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1812 | .INVALID_PARAMETER => unreachable, | |
| 1813 | else => return w.unexpectedStatus(rc), | |
| 1814 | } | |
| 1815 | } | |
| 1816 | ||
| 1817 | pub const DeleteFileError = os.UnlinkError; | |
| 1818 | ||
| 1819 | /// Delete a file name and possibly the file it refers to, based on an open directory handle. | |
| 1820 | /// Asserts that the path parameter has no null bytes. | |
| 1821 | pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { | |
| 1822 | if (builtin.os.tag == .windows) { | |
| 1823 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1824 | return self.deleteFileW(sub_path_w.span()); | |
| 1825 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1826 | os.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { | |
| 1827 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1828 | else => |e| return e, | |
| 1829 | }; | |
| 1830 | } else { | |
| 1831 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1832 | return self.deleteFileZ(&sub_path_c); | |
| 1833 | } | |
| 1834 | } | |
| 1835 | ||
| 1836 | /// Same as `deleteFile` except the parameter is null-terminated. | |
| 1837 | pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { | |
| 1838 | os.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { | |
| 1839 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1840 | error.AccessDenied => |e| switch (builtin.os.tag) { | |
| 1841 | // non-Linux POSIX systems return EPERM when trying to delete a directory, so | |
| 1842 | // we need to handle that case specifically and translate the error | |
| 1843 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { | |
| 1844 | // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) | |
| 1845 | const fstat = os.fstatatZ(self.fd, sub_path_c, os.AT.SYMLINK_NOFOLLOW) catch return e; | |
| 1846 | const is_dir = fstat.mode & os.S.IFMT == os.S.IFDIR; | |
| 1847 | return if (is_dir) error.IsDir else e; | |
| 1848 | }, | |
| 1849 | else => return e, | |
| 1850 | }, | |
| 1851 | else => |e| return e, | |
| 1852 | }; | |
| 1853 | } | |
| 1854 | ||
| 1855 | /// Same as `deleteFile` except the parameter is WTF-16 encoded. | |
| 1856 | pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { | |
| 1857 | os.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { | |
| 1858 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1859 | else => |e| return e, | |
| 1860 | }; | |
| 1861 | } | |
| 1862 | ||
| 1863 | pub const DeleteDirError = error{ | |
| 1864 | DirNotEmpty, | |
| 1865 | FileNotFound, | |
| 1866 | AccessDenied, | |
| 1867 | FileBusy, | |
| 1868 | FileSystem, | |
| 1869 | SymLinkLoop, | |
| 1870 | NameTooLong, | |
| 1871 | NotDir, | |
| 1872 | SystemResources, | |
| 1873 | ReadOnlyFileSystem, | |
| 1874 | InvalidUtf8, | |
| 1875 | BadPathName, | |
| 1876 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1877 | NetworkNotFound, | |
| 1878 | Unexpected, | |
| 1879 | }; | |
| 1880 | ||
| 1881 | /// Returns `error.DirNotEmpty` if the directory is not empty. | |
| 1882 | /// To delete a directory recursively, see `deleteTree`. | |
| 1883 | /// Asserts that the path parameter has no null bytes. | |
| 1884 | pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { | |
| 1885 | if (builtin.os.tag == .windows) { | |
| 1886 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1887 | return self.deleteDirW(sub_path_w.span()); | |
| 1888 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1889 | os.unlinkat(self.fd, sub_path, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1890 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1891 | else => |e| return e, | |
| 1892 | }; | |
| 1893 | } else { | |
| 1894 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 1895 | return self.deleteDirZ(&sub_path_c); | |
| 1896 | } | |
| 1897 | } | |
| 1898 | ||
| 1899 | /// Same as `deleteDir` except the parameter is null-terminated. | |
| 1900 | pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { | |
| 1901 | os.unlinkatZ(self.fd, sub_path_c, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1902 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1903 | else => |e| return e, | |
| 1904 | }; | |
| 1905 | } | |
| 1906 | ||
| 1907 | /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. | |
| 1908 | /// This function is Windows-only. | |
| 1909 | pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { | |
| 1910 | os.unlinkatW(self.fd, sub_path_w, os.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1911 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1912 | else => |e| return e, | |
| 1913 | }; | |
| 1914 | } | |
| 1915 | ||
| 1916 | pub const RenameError = os.RenameError; | |
| 1917 | ||
| 1918 | /// Change the name or location of a file or directory. | |
| 1919 | /// If new_sub_path already exists, it will be replaced. | |
| 1920 | /// Renaming a file over an existing directory or a directory | |
| 1921 | /// over an existing file will fail with `error.IsDir` or `error.NotDir` | |
| 1922 | pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { | |
| 1923 | return os.renameat(self.fd, old_sub_path, self.fd, new_sub_path); | |
| 1924 | } | |
| 1925 | ||
| 1926 | /// Same as `rename` except the parameters are null-terminated. | |
| 1927 | pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { | |
| 1928 | return os.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); | |
| 1929 | } | |
| 1930 | ||
| 1931 | /// Same as `rename` except the parameters are UTF16LE, NT prefixed. | |
| 1932 | /// This function is Windows-only. | |
| 1933 | pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { | |
| 1934 | return os.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); | |
| 1935 | } | |
| 1936 | ||
| 1937 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. | |
| 1938 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent | |
| 1939 | /// one; the latter case is known as a dangling link. | |
| 1940 | /// If `sym_link_path` exists, it will not be overwritten. | |
| 1941 | pub fn symLink( | |
| 1942 | self: Dir, | |
| 1943 | target_path: []const u8, | |
| 1944 | sym_link_path: []const u8, | |
| 1945 | flags: SymLinkFlags, | |
| 1946 | ) !void { | |
| 1947 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1948 | return self.symLinkWasi(target_path, sym_link_path, flags); | |
| 1949 | } | |
| 1950 | if (builtin.os.tag == .windows) { | |
| 1951 | // Target path does not use sliceToPrefixedFileW because certain paths | |
| 1952 | // are handled differently when creating a symlink than they would be | |
| 1953 | // when converting to an NT namespaced path. CreateSymbolicLink in | |
| 1954 | // symLinkW will handle the necessary conversion. | |
| 1955 | var target_path_w: os.windows.PathSpace = undefined; | |
| 1956 | target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); | |
| 1957 | target_path_w.data[target_path_w.len] = 0; | |
| 1958 | const sym_link_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); | |
| 1959 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1960 | } | |
| 1961 | const target_path_c = try os.toPosixPath(target_path); | |
| 1962 | const sym_link_path_c = try os.toPosixPath(sym_link_path); | |
| 1963 | return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); | |
| 1964 | } | |
| 1965 | ||
| 1966 | /// WASI-only. Same as `symLink` except targeting WASI. | |
| 1967 | pub fn symLinkWasi( | |
| 1968 | self: Dir, | |
| 1969 | target_path: []const u8, | |
| 1970 | sym_link_path: []const u8, | |
| 1971 | _: SymLinkFlags, | |
| 1972 | ) !void { | |
| 1973 | return os.symlinkat(target_path, self.fd, sym_link_path); | |
| 1974 | } | |
| 1975 | ||
| 1976 | /// Same as `symLink`, except the pathname parameters are null-terminated. | |
| 1977 | pub fn symLinkZ( | |
| 1978 | self: Dir, | |
| 1979 | target_path_c: [*:0]const u8, | |
| 1980 | sym_link_path_c: [*:0]const u8, | |
| 1981 | flags: SymLinkFlags, | |
| 1982 | ) !void { | |
| 1983 | if (builtin.os.tag == .windows) { | |
| 1984 | const target_path_w = try os.windows.cStrToPrefixedFileW(self.fd, target_path_c); | |
| 1985 | const sym_link_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); | |
| 1986 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1987 | } | |
| 1988 | return os.symlinkatZ(target_path_c, self.fd, sym_link_path_c); | |
| 1989 | } | |
| 1990 | ||
| 1991 | /// Windows-only. Same as `symLink` except the pathname parameters | |
| 1992 | /// are null-terminated, WTF16 encoded. | |
| 1993 | pub fn symLinkW( | |
| 1994 | self: Dir, | |
| 1995 | /// WTF-16, does not need to be NT-prefixed. The NT-prefixing | |
| 1996 | /// of this path is handled by CreateSymbolicLink. | |
| 1997 | target_path_w: [:0]const u16, | |
| 1998 | /// WTF-16, must be NT-prefixed or relative | |
| 1999 | sym_link_path_w: []const u16, | |
| 2000 | flags: SymLinkFlags, | |
| 2001 | ) !void { | |
| 2002 | return os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); | |
| 2003 | } | |
| 2004 | ||
| 2005 | pub const ReadLinkError = os.ReadLinkError; | |
| 2006 | ||
| 2007 | /// Read value of a symbolic link. | |
| 2008 | /// The return value is a slice of `buffer`, from index `0`. | |
| 2009 | /// Asserts that the path parameter has no null bytes. | |
| 2010 | pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { | |
| 2011 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2012 | return self.readLinkWasi(sub_path, buffer); | |
| 2013 | } | |
| 2014 | if (builtin.os.tag == .windows) { | |
| 2015 | const sub_path_w = try os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 2016 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 2017 | } | |
| 2018 | const sub_path_c = try os.toPosixPath(sub_path); | |
| 2019 | return self.readLinkZ(&sub_path_c, buffer); | |
| 2020 | } | |
| 2021 | ||
| 2022 | /// WASI-only. Same as `readLink` except targeting WASI. | |
| 2023 | pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { | |
| 2024 | return os.readlinkat(self.fd, sub_path, buffer); | |
| 2025 | } | |
| 2026 | ||
| 2027 | /// Same as `readLink`, except the `pathname` parameter is null-terminated. | |
| 2028 | pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { | |
| 2029 | if (builtin.os.tag == .windows) { | |
| 2030 | const sub_path_w = try os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 2031 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 2032 | } | |
| 2033 | return os.readlinkatZ(self.fd, sub_path_c, buffer); | |
| 2034 | } | |
| 2035 | ||
| 2036 | /// Windows-only. Same as `readLink` except the pathname parameter | |
| 2037 | /// is null-terminated, WTF16 encoded. | |
| 2038 | pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { | |
| 2039 | return os.windows.ReadLink(self.fd, sub_path_w, buffer); | |
| 2040 | } | |
| 2041 | ||
| 2042 | /// Read all of file contents using a preallocated buffer. | |
| 2043 | /// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` | |
| 2044 | /// the situation is ambiguous. It could either mean that the entire file was read, and | |
| 2045 | /// it exactly fits the buffer, or it could mean the buffer was not big enough for the | |
| 2046 | /// entire file. | |
| 2047 | pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { | |
| 2048 | var file = try self.openFile(file_path, .{}); | |
| 2049 | defer file.close(); | |
| 2050 | ||
| 2051 | const end_index = try file.readAll(buffer); | |
| 2052 | return buffer[0..end_index]; | |
| 2053 | } | |
| 2054 | ||
| 2055 | /// On success, caller owns returned buffer. | |
| 2056 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 2057 | pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { | |
| 2058 | return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); | |
| 2059 | } | |
| 2060 | ||
| 2061 | /// On success, caller owns returned buffer. | |
| 2062 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 2063 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 2064 | /// that value, otherwise the effective file size is used instead. | |
| 2065 | /// Allows specifying alignment and a sentinel value. | |
| 2066 | pub fn readFileAllocOptions( | |
| 2067 | self: Dir, | |
| 2068 | allocator: mem.Allocator, | |
| 2069 | file_path: []const u8, | |
| 2070 | max_bytes: usize, | |
| 2071 | size_hint: ?usize, | |
| 2072 | comptime alignment: u29, | |
| 2073 | comptime optional_sentinel: ?u8, | |
| 2074 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 2075 | var file = try self.openFile(file_path, .{}); | |
| 2076 | defer file.close(); | |
| 2077 | ||
| 2078 | // If the file size doesn't fit a usize it'll be certainly greater than | |
| 2079 | // `max_bytes` | |
| 2080 | const stat_size = size_hint orelse math.cast(usize, try file.getEndPos()) orelse | |
| 2081 | return error.FileTooBig; | |
| 2082 | ||
| 2083 | return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); | |
| 2084 | } | |
| 2085 | ||
| 2086 | pub const DeleteTreeError = error{ | |
| 2087 | InvalidHandle, | |
| 2088 | AccessDenied, | |
| 2089 | FileTooBig, | |
| 2090 | SymLinkLoop, | |
| 2091 | ProcessFdQuotaExceeded, | |
| 2092 | NameTooLong, | |
| 2093 | SystemFdQuotaExceeded, | |
| 2094 | NoDevice, | |
| 2095 | SystemResources, | |
| 2096 | ReadOnlyFileSystem, | |
| 2097 | FileSystem, | |
| 2098 | FileBusy, | |
| 2099 | DeviceBusy, | |
| 2100 | ||
| 2101 | /// One of the path components was not a directory. | |
| 2102 | /// This error is unreachable if `sub_path` does not contain a path separator. | |
| 2103 | NotDir, | |
| 2104 | ||
| 2105 | /// On Windows, file paths must be valid Unicode. | |
| 2106 | InvalidUtf8, | |
| 2107 | ||
| 2108 | /// On Windows, file paths cannot contain these characters: | |
| 2109 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 2110 | BadPathName, | |
| 2111 | ||
| 2112 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 2113 | NetworkNotFound, | |
| 2114 | } || os.UnexpectedError; | |
| 2115 | ||
| 2116 | /// Whether `full_path` describes a symlink, file, or directory, this function | |
| 2117 | /// removes it. If it cannot be removed because it is a non-empty directory, | |
| 2118 | /// this function recursively removes its entries and then tries again. | |
| 2119 | /// This operation is not atomic on most file systems. | |
| 2120 | pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2121 | var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; | |
| 2122 | ||
| 2123 | const StackItem = struct { | |
| 2124 | name: []const u8, | |
| 2125 | parent_dir: Dir, | |
| 2126 | iter: Dir.Iterator, | |
| 2127 | ||
| 2128 | fn closeAll(items: []@This()) void { | |
| 2129 | for (items) |*item| item.iter.dir.close(); | |
| 2130 | } | |
| 2131 | }; | |
| 2132 | ||
| 2133 | var stack_buffer: [16]StackItem = undefined; | |
| 2134 | var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); | |
| 2135 | defer StackItem.closeAll(stack.items); | |
| 2136 | ||
| 2137 | stack.appendAssumeCapacity(.{ | |
| 2138 | .name = sub_path, | |
| 2139 | .parent_dir = self, | |
| 2140 | .iter = initial_iterable_dir.iterateAssumeFirstIteration(), | |
| 2141 | }); | |
| 2142 | ||
| 2143 | process_stack: while (stack.items.len != 0) { | |
| 2144 | var top = &stack.items[stack.items.len - 1]; | |
| 2145 | while (try top.iter.next()) |entry| { | |
| 2146 | var treat_as_dir = entry.kind == .directory; | |
| 2147 | handle_entry: while (true) { | |
| 2148 | if (treat_as_dir) { | |
| 2149 | if (stack.unusedCapacitySlice().len >= 1) { | |
| 2150 | var iterable_dir = top.iter.dir.openDir(entry.name, .{ | |
| 2151 | .no_follow = true, | |
| 2152 | .iterate = true, | |
| 2153 | }) catch |err| switch (err) { | |
| 2154 | error.NotDir => { | |
| 2155 | treat_as_dir = false; | |
| 2156 | continue :handle_entry; | |
| 2157 | }, | |
| 2158 | error.FileNotFound => { | |
| 2159 | // That's fine, we were trying to remove this directory anyway. | |
| 2160 | break :handle_entry; | |
| 2161 | }, | |
| 2162 | ||
| 2163 | error.InvalidHandle, | |
| 2164 | error.AccessDenied, | |
| 2165 | error.SymLinkLoop, | |
| 2166 | error.ProcessFdQuotaExceeded, | |
| 2167 | error.NameTooLong, | |
| 2168 | error.SystemFdQuotaExceeded, | |
| 2169 | error.NoDevice, | |
| 2170 | error.SystemResources, | |
| 2171 | error.Unexpected, | |
| 2172 | error.InvalidUtf8, | |
| 2173 | error.BadPathName, | |
| 2174 | error.NetworkNotFound, | |
| 2175 | error.DeviceBusy, | |
| 2176 | => |e| return e, | |
| 2177 | }; | |
| 2178 | stack.appendAssumeCapacity(.{ | |
| 2179 | .name = entry.name, | |
| 2180 | .parent_dir = top.iter.dir, | |
| 2181 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2182 | }); | |
| 2183 | continue :process_stack; | |
| 2184 | } else { | |
| 2185 | try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); | |
| 2186 | break :handle_entry; | |
| 2187 | } | |
| 2188 | } else { | |
| 2189 | if (top.iter.dir.deleteFile(entry.name)) { | |
| 2190 | break :handle_entry; | |
| 2191 | } else |err| switch (err) { | |
| 2192 | error.FileNotFound => break :handle_entry, | |
| 2193 | ||
| 2194 | // Impossible because we do not pass any path separators. | |
| 2195 | error.NotDir => unreachable, | |
| 2196 | ||
| 2197 | error.IsDir => { | |
| 2198 | treat_as_dir = true; | |
| 2199 | continue :handle_entry; | |
| 2200 | }, | |
| 2201 | ||
| 2202 | error.AccessDenied, | |
| 2203 | error.InvalidUtf8, | |
| 2204 | error.SymLinkLoop, | |
| 2205 | error.NameTooLong, | |
| 2206 | error.SystemResources, | |
| 2207 | error.ReadOnlyFileSystem, | |
| 2208 | error.FileSystem, | |
| 2209 | error.FileBusy, | |
| 2210 | error.BadPathName, | |
| 2211 | error.NetworkNotFound, | |
| 2212 | error.Unexpected, | |
| 2213 | => |e| return e, | |
| 2214 | } | |
| 2215 | } | |
| 2216 | } | |
| 2217 | } | |
| 2218 | ||
| 2219 | // On Windows, we can't delete until the dir's handle has been closed, so | |
| 2220 | // close it before we try to delete. | |
| 2221 | top.iter.dir.close(); | |
| 2222 | ||
| 2223 | // In order to avoid double-closing the directory when cleaning up | |
| 2224 | // the stack in the case of an error, we save the relevant portions and | |
| 2225 | // pop the value from the stack. | |
| 2226 | const parent_dir = top.parent_dir; | |
| 2227 | const name = top.name; | |
| 2228 | stack.items.len -= 1; | |
| 2229 | ||
| 2230 | var need_to_retry: bool = false; | |
| 2231 | parent_dir.deleteDir(name) catch |err| switch (err) { | |
| 2232 | error.FileNotFound => {}, | |
| 2233 | error.DirNotEmpty => need_to_retry = true, | |
| 2234 | else => |e| return e, | |
| 2235 | }; | |
| 2236 | ||
| 2237 | if (need_to_retry) { | |
| 2238 | // Since we closed the handle that the previous iterator used, we | |
| 2239 | // need to re-open the dir and re-create the iterator. | |
| 2240 | var iterable_dir = iterable_dir: { | |
| 2241 | var treat_as_dir = true; | |
| 2242 | handle_entry: while (true) { | |
| 2243 | if (treat_as_dir) { | |
| 2244 | break :iterable_dir parent_dir.openDir(name, .{ | |
| 2245 | .no_follow = true, | |
| 2246 | .iterate = true, | |
| 2247 | }) catch |err| switch (err) { | |
| 2248 | error.NotDir => { | |
| 2249 | treat_as_dir = false; | |
| 2250 | continue :handle_entry; | |
| 2251 | }, | |
| 2252 | error.FileNotFound => { | |
| 2253 | // That's fine, we were trying to remove this directory anyway. | |
| 2254 | continue :process_stack; | |
| 2255 | }, | |
| 2256 | ||
| 2257 | error.InvalidHandle, | |
| 2258 | error.AccessDenied, | |
| 2259 | error.SymLinkLoop, | |
| 2260 | error.ProcessFdQuotaExceeded, | |
| 2261 | error.NameTooLong, | |
| 2262 | error.SystemFdQuotaExceeded, | |
| 2263 | error.NoDevice, | |
| 2264 | error.SystemResources, | |
| 2265 | error.Unexpected, | |
| 2266 | error.InvalidUtf8, | |
| 2267 | error.BadPathName, | |
| 2268 | error.NetworkNotFound, | |
| 2269 | error.DeviceBusy, | |
| 2270 | => |e| return e, | |
| 2271 | }; | |
| 2272 | } else { | |
| 2273 | if (parent_dir.deleteFile(name)) { | |
| 2274 | continue :process_stack; | |
| 2275 | } else |err| switch (err) { | |
| 2276 | error.FileNotFound => continue :process_stack, | |
| 2277 | ||
| 2278 | // Impossible because we do not pass any path separators. | |
| 2279 | error.NotDir => unreachable, | |
| 2280 | ||
| 2281 | error.IsDir => { | |
| 2282 | treat_as_dir = true; | |
| 2283 | continue :handle_entry; | |
| 2284 | }, | |
| 2285 | ||
| 2286 | error.AccessDenied, | |
| 2287 | error.InvalidUtf8, | |
| 2288 | error.SymLinkLoop, | |
| 2289 | error.NameTooLong, | |
| 2290 | error.SystemResources, | |
| 2291 | error.ReadOnlyFileSystem, | |
| 2292 | error.FileSystem, | |
| 2293 | error.FileBusy, | |
| 2294 | error.BadPathName, | |
| 2295 | error.NetworkNotFound, | |
| 2296 | error.Unexpected, | |
| 2297 | => |e| return e, | |
| 2298 | } | |
| 2299 | } | |
| 2300 | } | |
| 2301 | }; | |
| 2302 | // We know there is room on the stack since we are just re-adding | |
| 2303 | // the StackItem that we previously popped. | |
| 2304 | stack.appendAssumeCapacity(.{ | |
| 2305 | .name = name, | |
| 2306 | .parent_dir = parent_dir, | |
| 2307 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2308 | }); | |
| 2309 | continue :process_stack; | |
| 2310 | } | |
| 2311 | } | |
| 2312 | } | |
| 2313 | ||
| 2314 | /// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. | |
| 2315 | /// This is slower than `deleteTree` but uses less stack space. | |
| 2316 | pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2317 | return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); | |
| 2318 | } | |
| 2319 | ||
| 2320 | fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { | |
| 2321 | start_over: while (true) { | |
| 2322 | var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; | |
| 2323 | var cleanup_dir_parent: ?Dir = null; | |
| 2324 | defer if (cleanup_dir_parent) |*d| d.close(); | |
| 2325 | ||
| 2326 | var cleanup_dir = true; | |
| 2327 | defer if (cleanup_dir) dir.close(); | |
| 2328 | ||
| 2329 | // Valid use of MAX_PATH_BYTES because dir_name_buf will only | |
| 2330 | // ever store a single path component that was returned from the | |
| 2331 | // filesystem. | |
| 2332 | var dir_name_buf: [MAX_PATH_BYTES]u8 = undefined; | |
| 2333 | var dir_name: []const u8 = sub_path; | |
| 2334 | ||
| 2335 | // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. | |
| 2336 | // Go through each entry and if it is not a directory, delete it. If it is a directory, | |
| 2337 | // open it, and close the original directory. Repeat. Then start the entire operation over. | |
| 2338 | ||
| 2339 | scan_dir: while (true) { | |
| 2340 | var dir_it = dir.iterateAssumeFirstIteration(); | |
| 2341 | dir_it: while (try dir_it.next()) |entry| { | |
| 2342 | var treat_as_dir = entry.kind == .directory; | |
| 2343 | handle_entry: while (true) { | |
| 2344 | if (treat_as_dir) { | |
| 2345 | const new_dir = dir.openDir(entry.name, .{ | |
| 2346 | .no_follow = true, | |
| 2347 | .iterate = true, | |
| 2348 | }) catch |err| switch (err) { | |
| 2349 | error.NotDir => { | |
| 2350 | treat_as_dir = false; | |
| 2351 | continue :handle_entry; | |
| 2352 | }, | |
| 2353 | error.FileNotFound => { | |
| 2354 | // That's fine, we were trying to remove this directory anyway. | |
| 2355 | continue :dir_it; | |
| 2356 | }, | |
| 2357 | ||
| 2358 | error.InvalidHandle, | |
| 2359 | error.AccessDenied, | |
| 2360 | error.SymLinkLoop, | |
| 2361 | error.ProcessFdQuotaExceeded, | |
| 2362 | error.NameTooLong, | |
| 2363 | error.SystemFdQuotaExceeded, | |
| 2364 | error.NoDevice, | |
| 2365 | error.SystemResources, | |
| 2366 | error.Unexpected, | |
| 2367 | error.InvalidUtf8, | |
| 2368 | error.BadPathName, | |
| 2369 | error.NetworkNotFound, | |
| 2370 | error.DeviceBusy, | |
| 2371 | => |e| return e, | |
| 2372 | }; | |
| 2373 | if (cleanup_dir_parent) |*d| d.close(); | |
| 2374 | cleanup_dir_parent = dir; | |
| 2375 | dir = new_dir; | |
| 2376 | const result = dir_name_buf[0..entry.name.len]; | |
| 2377 | @memcpy(result, entry.name); | |
| 2378 | dir_name = result; | |
| 2379 | continue :scan_dir; | |
| 2380 | } else { | |
| 2381 | if (dir.deleteFile(entry.name)) { | |
| 2382 | continue :dir_it; | |
| 2383 | } else |err| switch (err) { | |
| 2384 | error.FileNotFound => continue :dir_it, | |
| 2385 | ||
| 2386 | // Impossible because we do not pass any path separators. | |
| 2387 | error.NotDir => unreachable, | |
| 2388 | ||
| 2389 | error.IsDir => { | |
| 2390 | treat_as_dir = true; | |
| 2391 | continue :handle_entry; | |
| 2392 | }, | |
| 2393 | ||
| 2394 | error.AccessDenied, | |
| 2395 | error.InvalidUtf8, | |
| 2396 | error.SymLinkLoop, | |
| 2397 | error.NameTooLong, | |
| 2398 | error.SystemResources, | |
| 2399 | error.ReadOnlyFileSystem, | |
| 2400 | error.FileSystem, | |
| 2401 | error.FileBusy, | |
| 2402 | error.BadPathName, | |
| 2403 | error.NetworkNotFound, | |
| 2404 | error.Unexpected, | |
| 2405 | => |e| return e, | |
| 2406 | } | |
| 2407 | } | |
| 2408 | } | |
| 2409 | } | |
| 2410 | // Reached the end of the directory entries, which means we successfully deleted all of them. | |
| 2411 | // Now to remove the directory itself. | |
| 2412 | dir.close(); | |
| 2413 | cleanup_dir = false; | |
| 2414 | ||
| 2415 | if (cleanup_dir_parent) |d| { | |
| 2416 | d.deleteDir(dir_name) catch |err| switch (err) { | |
| 2417 | // These two things can happen due to file system race conditions. | |
| 2418 | error.FileNotFound, error.DirNotEmpty => continue :start_over, | |
| 2419 | else => |e| return e, | |
| 2420 | }; | |
| 2421 | continue :start_over; | |
| 2422 | } else { | |
| 2423 | self.deleteDir(sub_path) catch |err| switch (err) { | |
| 2424 | error.FileNotFound => return, | |
| 2425 | error.DirNotEmpty => continue :start_over, | |
| 2426 | else => |e| return e, | |
| 2427 | }; | |
| 2428 | return; | |
| 2429 | } | |
| 2430 | } | |
| 2431 | } | |
| 2432 | } | |
| 2433 | ||
| 2434 | /// On successful delete, returns null. | |
| 2435 | fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { | |
| 2436 | return iterable_dir: { | |
| 2437 | // Treat as a file by default | |
| 2438 | var treat_as_dir = kind_hint == .directory; | |
| 2439 | ||
| 2440 | handle_entry: while (true) { | |
| 2441 | if (treat_as_dir) { | |
| 2442 | break :iterable_dir self.openDir(sub_path, .{ | |
| 2443 | .no_follow = true, | |
| 2444 | .iterate = true, | |
| 2445 | }) catch |err| switch (err) { | |
| 2446 | error.NotDir => { | |
| 2447 | treat_as_dir = false; | |
| 2448 | continue :handle_entry; | |
| 2449 | }, | |
| 2450 | error.FileNotFound => { | |
| 2451 | // That's fine, we were trying to remove this directory anyway. | |
| 2452 | return null; | |
| 2453 | }, | |
| 2454 | ||
| 2455 | error.InvalidHandle, | |
| 2456 | error.AccessDenied, | |
| 2457 | error.SymLinkLoop, | |
| 2458 | error.ProcessFdQuotaExceeded, | |
| 2459 | error.NameTooLong, | |
| 2460 | error.SystemFdQuotaExceeded, | |
| 2461 | error.NoDevice, | |
| 2462 | error.SystemResources, | |
| 2463 | error.Unexpected, | |
| 2464 | error.InvalidUtf8, | |
| 2465 | error.BadPathName, | |
| 2466 | error.DeviceBusy, | |
| 2467 | error.NetworkNotFound, | |
| 2468 | => |e| return e, | |
| 2469 | }; | |
| 2470 | } else { | |
| 2471 | if (self.deleteFile(sub_path)) { | |
| 2472 | return null; | |
| 2473 | } else |err| switch (err) { | |
| 2474 | error.FileNotFound => return null, | |
| 2475 | ||
| 2476 | error.IsDir => { | |
| 2477 | treat_as_dir = true; | |
| 2478 | continue :handle_entry; | |
| 2479 | }, | |
| 2480 | ||
| 2481 | error.AccessDenied, | |
| 2482 | error.InvalidUtf8, | |
| 2483 | error.SymLinkLoop, | |
| 2484 | error.NameTooLong, | |
| 2485 | error.SystemResources, | |
| 2486 | error.ReadOnlyFileSystem, | |
| 2487 | error.NotDir, | |
| 2488 | error.FileSystem, | |
| 2489 | error.FileBusy, | |
| 2490 | error.BadPathName, | |
| 2491 | error.NetworkNotFound, | |
| 2492 | error.Unexpected, | |
| 2493 | => |e| return e, | |
| 2494 | } | |
| 2495 | } | |
| 2496 | } | |
| 2497 | }; | |
| 2498 | } | |
| 2499 | ||
| 2500 | pub const WriteFileError = File.WriteError || File.OpenError; | |
| 2501 | ||
| 2502 | /// Deprecated: use `writeFile2`. | |
| 2503 | pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { | |
| 2504 | return writeFile2(self, .{ | |
| 2505 | .sub_path = sub_path, | |
| 2506 | .data = data, | |
| 2507 | .flags = .{}, | |
| 2508 | }); | |
| 2509 | } | |
| 2510 | ||
| 2511 | pub const WriteFileOptions = struct { | |
| 2512 | sub_path: []const u8, | |
| 2513 | data: []const u8, | |
| 2514 | flags: File.CreateFlags = .{}, | |
| 2515 | }; | |
| 2516 | ||
| 2517 | /// Writes content to the file system, using the file creation flags provided. | |
| 2518 | pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { | |
| 2519 | var file = try self.createFile(options.sub_path, options.flags); | |
| 2520 | defer file.close(); | |
| 2521 | try file.writeAll(options.data); | |
| 2522 | } | |
| 2523 | ||
| 2524 | pub const AccessError = os.AccessError; | |
| 2525 | ||
| 2526 | /// Test accessing `path`. | |
| 2527 | /// `path` is UTF-8-encoded. | |
| 2528 | /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. | |
| 2529 | /// For example, instead of testing if a file exists and then opening it, just | |
| 2530 | /// open it and handle the error for file not found. | |
| 2531 | pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { | |
| 2532 | if (builtin.os.tag == .windows) { | |
| 2533 | const sub_path_w = os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2534 | error.AccessDenied => return error.PermissionDenied, | |
| 2535 | else => |e| return e, | |
| 2536 | }; | |
| 2537 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2538 | } | |
| 2539 | const path_c = try os.toPosixPath(sub_path); | |
| 2540 | return self.accessZ(&path_c, flags); | |
| 2541 | } | |
| 2542 | ||
| 2543 | /// Same as `access` except the path parameter is null-terminated. | |
| 2544 | pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { | |
| 2545 | if (builtin.os.tag == .windows) { | |
| 2546 | const sub_path_w = os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2547 | error.AccessDenied => return error.PermissionDenied, | |
| 2548 | else => |e| return e, | |
| 2549 | }; | |
| 2550 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2551 | } | |
| 2552 | const os_mode = switch (flags.mode) { | |
| 2553 | .read_only => @as(u32, os.F_OK), | |
| 2554 | .write_only => @as(u32, os.W_OK), | |
| 2555 | .read_write => @as(u32, os.R_OK | os.W_OK), | |
| 2556 | }; | |
| 2557 | const result = if (need_async_thread and flags.intended_io_mode != .blocking) | |
| 2558 | std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) | |
| 2559 | else | |
| 2560 | os.faccessatZ(self.fd, sub_path, os_mode, 0); | |
| 2561 | return result; | |
| 2562 | } | |
| 2563 | ||
| 2564 | /// Same as `access` except asserts the target OS is Windows and the path parameter is | |
| 2565 | /// * WTF-16 encoded | |
| 2566 | /// * null-terminated | |
| 2567 | /// * NtDll prefixed | |
| 2568 | /// TODO currently this ignores `flags`. | |
| 2569 | pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { | |
| 2570 | _ = flags; | |
| 2571 | return os.faccessatW(self.fd, sub_path_w, 0, 0); | |
| 2572 | } | |
| 2573 | ||
| 2574 | /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. | |
| 2575 | /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, | |
| 2576 | /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. | |
| 2577 | /// Returns the previous status of the file before updating. | |
| 2578 | /// If any of the directories do not exist for dest_path, they are created. | |
| 2579 | pub fn updateFile( | |
| 2580 | source_dir: Dir, | |
| 2581 | source_path: []const u8, | |
| 2582 | dest_dir: Dir, | |
| 2583 | dest_path: []const u8, | |
| 2584 | options: CopyFileOptions, | |
| 2585 | ) !PrevStatus { | |
| 2586 | var src_file = try source_dir.openFile(source_path, .{}); | |
| 2587 | defer src_file.close(); | |
| 2588 | ||
| 2589 | const src_stat = try src_file.stat(); | |
| 2590 | const actual_mode = options.override_mode orelse src_stat.mode; | |
| 2591 | check_dest_stat: { | |
| 2592 | const dest_stat = blk: { | |
| 2593 | var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { | |
| 2594 | error.FileNotFound => break :check_dest_stat, | |
| 2595 | else => |e| return e, | |
| 2596 | }; | |
| 2597 | defer dest_file.close(); | |
| 2598 | ||
| 2599 | break :blk try dest_file.stat(); | |
| 2600 | }; | |
| 2601 | ||
| 2602 | if (src_stat.size == dest_stat.size and | |
| 2603 | src_stat.mtime == dest_stat.mtime and | |
| 2604 | actual_mode == dest_stat.mode) | |
| 2605 | { | |
| 2606 | return PrevStatus.fresh; | |
| 2607 | } | |
| 2608 | } | |
| 2609 | ||
| 2610 | if (path.dirname(dest_path)) |dirname| { | |
| 2611 | try dest_dir.makePath(dirname); | |
| 2612 | } | |
| 2613 | ||
| 2614 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); | |
| 2615 | defer atomic_file.deinit(); | |
| 2616 | ||
| 2617 | try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); | |
| 2618 | try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); | |
| 2619 | try atomic_file.finish(); | |
| 2620 | return PrevStatus.stale; | |
| 2621 | } | |
| 2622 | ||
| 2623 | pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; | |
| 2624 | ||
| 2625 | /// Guaranteed to be atomic. | |
| 2626 | /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, | |
| 2627 | /// there is a possibility of power loss or application termination leaving temporary files present | |
| 2628 | /// in the same directory as dest_path. | |
| 2629 | pub fn copyFile(source_dir: Dir, source_path: []const u8, dest_dir: Dir, dest_path: []const u8, options: CopyFileOptions) CopyFileError!void { | |
| 2630 | var in_file = try source_dir.openFile(source_path, .{}); | |
| 2631 | defer in_file.close(); | |
| 2632 | ||
| 2633 | var size: ?u64 = null; | |
| 2634 | const mode = options.override_mode orelse blk: { | |
| 2635 | const st = try in_file.stat(); | |
| 2636 | size = st.size; | |
| 2637 | break :blk st.mode; | |
| 2638 | }; | |
| 2639 | ||
| 2640 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); | |
| 2641 | defer atomic_file.deinit(); | |
| 2642 | ||
| 2643 | try copy_file(in_file.handle, atomic_file.file.handle, size); | |
| 2644 | try atomic_file.finish(); | |
| 2645 | } | |
| 2646 | ||
| 2647 | pub const AtomicFileOptions = struct { | |
| 2648 | mode: File.Mode = File.default_mode, | |
| 2649 | }; | |
| 2650 | ||
| 2651 | /// Directly access the `.file` field, and then call `AtomicFile.finish` | |
| 2652 | /// to atomically replace `dest_path` with contents. | |
| 2653 | /// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. | |
| 2654 | /// `dest_path` must remain valid until `AtomicFile.deinit` is called. | |
| 2655 | pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { | |
| 2656 | if (path.dirname(dest_path)) |dirname| { | |
| 2657 | const dir = try self.openDir(dirname, .{}); | |
| 2658 | return AtomicFile.init(path.basename(dest_path), options.mode, dir, true); | |
| 2659 | } else { | |
| 2660 | return AtomicFile.init(dest_path, options.mode, self, false); | |
| 2661 | } | |
| 2662 | } | |
| 2663 | ||
| 2664 | pub const Stat = File.Stat; | |
| 2665 | pub const StatError = File.StatError; | |
| 2666 | ||
| 2667 | pub fn stat(self: Dir) StatError!Stat { | |
| 2668 | const file: File = .{ | |
| 2669 | .handle = self.fd, | |
| 2670 | .capable_io_mode = .blocking, | |
| 2671 | }; | |
| 2672 | return file.stat(); | |
| 2673 | } | |
| 2674 | ||
| 2675 | pub const StatFileError = File.OpenError || File.StatError || os.FStatAtError; | |
| 2676 | ||
| 2677 | /// Returns metadata for a file inside the directory. | |
| 2678 | /// | |
| 2679 | /// On Windows, this requires three syscalls. On other operating systems, it | |
| 2680 | /// only takes one. | |
| 2681 | /// | |
| 2682 | /// Symlinks are followed. | |
| 2683 | /// | |
| 2684 | /// `sub_path` may be absolute, in which case `self` is ignored. | |
| 2685 | pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { | |
| 2686 | if (builtin.os.tag == .windows) { | |
| 2687 | var file = try self.openFile(sub_path, .{}); | |
| 2688 | defer file.close(); | |
| 2689 | return file.stat(); | |
| 2690 | } | |
| 2691 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2692 | const st = try os.fstatatWasi(self.fd, sub_path, os.wasi.LOOKUP_SYMLINK_FOLLOW); | |
| 2693 | return Stat.fromSystem(st); | |
| 2694 | } | |
| 2695 | const st = try os.fstatat(self.fd, sub_path, 0); | |
| 2696 | return Stat.fromSystem(st); | |
| 2697 | } | |
| 2698 | ||
| 2699 | pub const ChmodError = File.ChmodError; | |
| 2700 | ||
| 2701 | /// Changes the mode of the directory. | |
| 2702 | /// The process must have the correct privileges in order to do this | |
| 2703 | /// successfully, or must have the effective user ID matching the owner | |
| 2704 | /// of the directory. Additionally, the directory must have been opened | |
| 2705 | /// with `OpenDirOptions{ .iterate = true }`. | |
| 2706 | pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { | |
| 2707 | const file: File = .{ | |
| 2708 | .handle = self.fd, | |
| 2709 | .capable_io_mode = .blocking, | |
| 2710 | }; | |
| 2711 | try file.chmod(new_mode); | |
| 2712 | } | |
| 2713 | ||
| 2714 | /// Changes the owner and group of the directory. | |
| 2715 | /// The process must have the correct privileges in order to do this | |
| 2716 | /// successfully. The group may be changed by the owner of the directory to | |
| 2717 | /// any group of which the owner is a member. Additionally, the directory | |
| 2718 | /// must have been opened with `OpenDirOptions{ .iterate = true }`. If the | |
| 2719 | /// owner or group is specified as `null`, the ID is not changed. | |
| 2720 | pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { | |
| 2721 | const file: File = .{ | |
| 2722 | .handle = self.fd, | |
| 2723 | .capable_io_mode = .blocking, | |
| 2724 | }; | |
| 2725 | try file.chown(owner, group); | |
| 2726 | } | |
| 2727 | ||
| 2728 | pub const ChownError = File.ChownError; | |
| 2729 | ||
| 2730 | const Permissions = File.Permissions; | |
| 2731 | pub const SetPermissionsError = File.SetPermissionsError; | |
| 2732 | ||
| 2733 | /// Sets permissions according to the provided `Permissions` struct. | |
| 2734 | /// This method is *NOT* available on WASI | |
| 2735 | pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { | |
| 2736 | const file: File = .{ | |
| 2737 | .handle = self.fd, | |
| 2738 | .capable_io_mode = .blocking, | |
| 2739 | }; | |
| 2740 | try file.setPermissions(permissions); | |
| 2741 | } | |
| 2742 | ||
| 2743 | const Metadata = File.Metadata; | |
| 2744 | pub const MetadataError = File.MetadataError; | |
| 2745 | ||
| 2746 | /// Returns a `Metadata` struct, representing the permissions on the directory | |
| 2747 | pub fn metadata(self: Dir) MetadataError!Metadata { | |
| 2748 | const file: File = .{ | |
| 2749 | .handle = self.fd, | |
| 2750 | .capable_io_mode = .blocking, | |
| 2751 | }; | |
| 2752 | return try file.metadata(); | |
| 2753 | } | |
| 2754 | }; | |
| 2755 | ||
| 2756 | 307 | /// Returns a handle to the current working directory. It is not opened with iteration capability. |
| 2757 | 308 | /// Closing the returned `Dir` is checked illegal behavior. Iterating over the result is illegal behavior. |
| 2758 | 309 | /// On POSIX targets, this function is comptime-callable. |
| ... | ... | @@ -2918,20 +469,16 @@ pub fn readLinkAbsoluteZ(pathname_c: [*:0]const u8, buffer: *[MAX_PATH_BYTES]u8) |
| 2918 | 469 | return os.readlinkZ(pathname_c, buffer); |
| 2919 | 470 | } |
| 2920 | 471 | |
| 2921 | /// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink | |
| 2922 | /// will point to a file or a directory. This value is ignored on all hosts | |
| 2923 | /// except Windows where creating symlinks to different resource types, requires | |
| 2924 | /// different flags. By default, `symLinkAbsolute` is assumed to point to a file. | |
| 2925 | pub const SymLinkFlags = struct { | |
| 2926 | is_directory: bool = false, | |
| 2927 | }; | |
| 2928 | ||
| 2929 | 472 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. |
| 2930 | 473 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent |
| 2931 | 474 | /// one; the latter case is known as a dangling link. |
| 2932 | 475 | /// If `sym_link_path` exists, it will not be overwritten. |
| 2933 | 476 | /// See also `symLinkAbsoluteZ` and `symLinkAbsoluteW`. |
| 2934 | pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags: SymLinkFlags) !void { | |
| 477 | pub fn symLinkAbsolute( | |
| 478 | target_path: []const u8, | |
| 479 | sym_link_path: []const u8, | |
| 480 | flags: Dir.SymLinkFlags, | |
| 481 | ) !void { | |
| 2935 | 482 | assert(path.isAbsolute(target_path)); |
| 2936 | 483 | assert(path.isAbsolute(sym_link_path)); |
| 2937 | 484 | if (builtin.os.tag == .windows) { |
| ... | ... | @@ -2946,7 +493,11 @@ pub fn symLinkAbsolute(target_path: []const u8, sym_link_path: []const u8, flags |
| 2946 | 493 | /// Note that this function will by default try creating a symbolic link to a file. If you would |
| 2947 | 494 | /// like to create a symbolic link to a directory, specify this with `SymLinkFlags{ .is_directory = true }`. |
| 2948 | 495 | /// See also `symLinkAbsolute`, `symLinkAbsoluteZ`. |
| 2949 | pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16, flags: SymLinkFlags) !void { | |
| 496 | pub fn symLinkAbsoluteW( | |
| 497 | target_path_w: []const u16, | |
| 498 | sym_link_path_w: []const u16, | |
| 499 | flags: Dir.SymLinkFlags, | |
| 500 | ) !void { | |
| 2950 | 501 | assert(path.isAbsoluteWindowsWTF16(target_path_w)); |
| 2951 | 502 | assert(path.isAbsoluteWindowsWTF16(sym_link_path_w)); |
| 2952 | 503 | return os.windows.CreateSymbolicLink(null, sym_link_path_w, target_path_w, flags.is_directory); |
| ... | ... | @@ -2954,7 +505,11 @@ pub fn symLinkAbsoluteW(target_path_w: []const u16, sym_link_path_w: []const u16 |
| 2954 | 505 | |
| 2955 | 506 | /// Same as `symLinkAbsolute` except the parameters are null-terminated pointers. |
| 2956 | 507 | /// See also `symLinkAbsolute`. |
| 2957 | pub fn symLinkAbsoluteZ(target_path_c: [*:0]const u8, sym_link_path_c: [*:0]const u8, flags: SymLinkFlags) !void { | |
| 508 | pub fn symLinkAbsoluteZ( | |
| 509 | target_path_c: [*:0]const u8, | |
| 510 | sym_link_path_c: [*:0]const u8, | |
| 511 | flags: Dir.SymLinkFlags, | |
| 512 | ) !void { | |
| 2958 | 513 | assert(path.isAbsoluteZ(target_path_c)); |
| 2959 | 514 | assert(path.isAbsoluteZ(sym_link_path_c)); |
| 2960 | 515 | if (builtin.os.tag == .windows) { |
| ... | ... | @@ -3151,59 +706,6 @@ pub fn realpathAlloc(allocator: Allocator, pathname: []const u8) ![]u8 { |
| 3151 | 706 | return allocator.dupe(u8, try os.realpath(pathname, &buf)); |
| 3152 | 707 | } |
| 3153 | 708 | |
| 3154 | const CopyFileRawError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError; | |
| 3155 | ||
| 3156 | // Transfer all the data between two file descriptors in the most efficient way. | |
| 3157 | // The copy starts at offset 0, the initial offsets are preserved. | |
| 3158 | // No metadata is transferred over. | |
| 3159 | fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t, maybe_size: ?u64) CopyFileRawError!void { | |
| 3160 | if (comptime builtin.target.isDarwin()) { | |
| 3161 | const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA); | |
| 3162 | switch (os.errno(rc)) { | |
| 3163 | .SUCCESS => return, | |
| 3164 | .INVAL => unreachable, | |
| 3165 | .NOMEM => return error.SystemResources, | |
| 3166 | // The source file is not a directory, symbolic link, or regular file. | |
| 3167 | // Try with the fallback path before giving up. | |
| 3168 | .OPNOTSUPP => {}, | |
| 3169 | else => |err| return os.unexpectedErrno(err), | |
| 3170 | } | |
| 3171 | } | |
| 3172 | ||
| 3173 | if (builtin.os.tag == .linux) { | |
| 3174 | // Try copy_file_range first as that works at the FS level and is the | |
| 3175 | // most efficient method (if available). | |
| 3176 | var offset: u64 = 0; | |
| 3177 | cfr_loop: while (true) { | |
| 3178 | // The kernel checks the u64 value `offset+count` for overflow, use | |
| 3179 | // a 32 bit value so that the syscall won't return EINVAL except for | |
| 3180 | // impossibly large files (> 2^64-1 - 2^32-1). | |
| 3181 | const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0); | |
| 3182 | // Terminate as soon as we have copied size bytes or no bytes | |
| 3183 | if (maybe_size) |s| { | |
| 3184 | if (s == amt) break :cfr_loop; | |
| 3185 | } | |
| 3186 | if (amt == 0) break :cfr_loop; | |
| 3187 | offset += amt; | |
| 3188 | } | |
| 3189 | return; | |
| 3190 | } | |
| 3191 | ||
| 3192 | // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the | |
| 3193 | // fallback code will copy the contents chunk by chunk. | |
| 3194 | const empty_iovec = [0]os.iovec_const{}; | |
| 3195 | var offset: u64 = 0; | |
| 3196 | sendfile_loop: while (true) { | |
| 3197 | const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); | |
| 3198 | // Terminate as soon as we have copied size bytes or no bytes | |
| 3199 | if (maybe_size) |s| { | |
| 3200 | if (s == amt) break :sendfile_loop; | |
| 3201 | } | |
| 3202 | if (amt == 0) break :sendfile_loop; | |
| 3203 | offset += amt; | |
| 3204 | } | |
| 3205 | } | |
| 3206 | ||
| 3207 | 709 | test { |
| 3208 | 710 | if (builtin.os.tag != .wasi) { |
| 3209 | 711 | _ = &makeDirAbsolute; |
| ... | ... | @@ -3211,10 +713,10 @@ test { |
| 3211 | 713 | _ = &copyFileAbsolute; |
| 3212 | 714 | _ = &updateFileAbsolute; |
| 3213 | 715 | } |
| 3214 | _ = &Dir.copyFile; | |
| 716 | _ = &File; | |
| 717 | _ = &Dir; | |
| 718 | _ = &path; | |
| 3215 | 719 | _ = @import("fs/test.zig"); |
| 3216 | _ = @import("fs/path.zig"); | |
| 3217 | _ = @import("fs/file.zig"); | |
| 3218 | 720 | _ = @import("fs/get_app_data_dir.zig"); |
| 3219 | 721 | _ = @import("fs/watch.zig"); |
| 3220 | 722 | } |
lib/std/fs/Dir.zig created+2533| ... | ... | @@ -0,0 +1,2533 @@ |
| 1 | fd: posix.fd_t, | |
| 2 | ||
| 3 | pub const default_mode = 0o755; | |
| 4 | ||
| 5 | pub const Entry = struct { | |
| 6 | name: []const u8, | |
| 7 | kind: Kind, | |
| 8 | ||
| 9 | pub const Kind = File.Kind; | |
| 10 | }; | |
| 11 | ||
| 12 | const IteratorError = error{ AccessDenied, SystemResources } || posix.UnexpectedError; | |
| 13 | ||
| 14 | pub const Iterator = switch (builtin.os.tag) { | |
| 15 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => struct { | |
| 16 | dir: Dir, | |
| 17 | seek: i64, | |
| 18 | buf: [1024]u8, // TODO align(@alignOf(posix.system.dirent)), | |
| 19 | index: usize, | |
| 20 | end_index: usize, | |
| 21 | first_iter: bool, | |
| 22 | ||
| 23 | const Self = @This(); | |
| 24 | ||
| 25 | pub const Error = IteratorError; | |
| 26 | ||
| 27 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 28 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 29 | pub fn next(self: *Self) Error!?Entry { | |
| 30 | switch (builtin.os.tag) { | |
| 31 | .macos, .ios => return self.nextDarwin(), | |
| 32 | .freebsd, .netbsd, .dragonfly, .openbsd => return self.nextBsd(), | |
| 33 | .solaris, .illumos => return self.nextSolaris(), | |
| 34 | else => @compileError("unimplemented"), | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | fn nextDarwin(self: *Self) !?Entry { | |
| 39 | start_over: while (true) { | |
| 40 | if (self.index >= self.end_index) { | |
| 41 | if (self.first_iter) { | |
| 42 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 43 | self.first_iter = false; | |
| 44 | } | |
| 45 | const rc = posix.system.__getdirentries64( | |
| 46 | self.dir.fd, | |
| 47 | &self.buf, | |
| 48 | self.buf.len, | |
| 49 | &self.seek, | |
| 50 | ); | |
| 51 | if (rc == 0) return null; | |
| 52 | if (rc < 0) { | |
| 53 | switch (posix.errno(rc)) { | |
| 54 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 55 | .FAULT => unreachable, | |
| 56 | .NOTDIR => unreachable, | |
| 57 | .INVAL => unreachable, | |
| 58 | else => |err| return posix.unexpectedErrno(err), | |
| 59 | } | |
| 60 | } | |
| 61 | self.index = 0; | |
| 62 | self.end_index = @as(usize, @intCast(rc)); | |
| 63 | } | |
| 64 | const darwin_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 65 | const next_index = self.index + darwin_entry.reclen(); | |
| 66 | self.index = next_index; | |
| 67 | ||
| 68 | const name = @as([*]u8, @ptrCast(&darwin_entry.d_name))[0..darwin_entry.d_namlen]; | |
| 69 | ||
| 70 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (darwin_entry.d_ino == 0)) { | |
| 71 | continue :start_over; | |
| 72 | } | |
| 73 | ||
| 74 | const entry_kind: Entry.Kind = switch (darwin_entry.d_type) { | |
| 75 | posix.DT.BLK => .block_device, | |
| 76 | posix.DT.CHR => .character_device, | |
| 77 | posix.DT.DIR => .directory, | |
| 78 | posix.DT.FIFO => .named_pipe, | |
| 79 | posix.DT.LNK => .sym_link, | |
| 80 | posix.DT.REG => .file, | |
| 81 | posix.DT.SOCK => .unix_domain_socket, | |
| 82 | posix.DT.WHT => .whiteout, | |
| 83 | else => .unknown, | |
| 84 | }; | |
| 85 | return Entry{ | |
| 86 | .name = name, | |
| 87 | .kind = entry_kind, | |
| 88 | }; | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | fn nextSolaris(self: *Self) !?Entry { | |
| 93 | start_over: while (true) { | |
| 94 | if (self.index >= self.end_index) { | |
| 95 | if (self.first_iter) { | |
| 96 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 97 | self.first_iter = false; | |
| 98 | } | |
| 99 | const rc = posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 100 | switch (posix.errno(rc)) { | |
| 101 | .SUCCESS => {}, | |
| 102 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 103 | .FAULT => unreachable, | |
| 104 | .NOTDIR => unreachable, | |
| 105 | .INVAL => unreachable, | |
| 106 | else => |err| return posix.unexpectedErrno(err), | |
| 107 | } | |
| 108 | if (rc == 0) return null; | |
| 109 | self.index = 0; | |
| 110 | self.end_index = @as(usize, @intCast(rc)); | |
| 111 | } | |
| 112 | const entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 113 | const next_index = self.index + entry.reclen(); | |
| 114 | self.index = next_index; | |
| 115 | ||
| 116 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&entry.d_name)), 0); | |
| 117 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) | |
| 118 | continue :start_over; | |
| 119 | ||
| 120 | // Solaris dirent doesn't expose d_type, so we have to call stat to get it. | |
| 121 | const stat_info = posix.fstatat( | |
| 122 | self.dir.fd, | |
| 123 | name, | |
| 124 | posix.AT.SYMLINK_NOFOLLOW, | |
| 125 | ) catch |err| switch (err) { | |
| 126 | error.NameTooLong => unreachable, | |
| 127 | error.SymLinkLoop => unreachable, | |
| 128 | error.FileNotFound => unreachable, // lost the race | |
| 129 | else => |e| return e, | |
| 130 | }; | |
| 131 | const entry_kind: Entry.Kind = switch (stat_info.mode & posix.S.IFMT) { | |
| 132 | posix.S.IFIFO => .named_pipe, | |
| 133 | posix.S.IFCHR => .character_device, | |
| 134 | posix.S.IFDIR => .directory, | |
| 135 | posix.S.IFBLK => .block_device, | |
| 136 | posix.S.IFREG => .file, | |
| 137 | posix.S.IFLNK => .sym_link, | |
| 138 | posix.S.IFSOCK => .unix_domain_socket, | |
| 139 | posix.S.IFDOOR => .door, | |
| 140 | posix.S.IFPORT => .event_port, | |
| 141 | else => .unknown, | |
| 142 | }; | |
| 143 | return Entry{ | |
| 144 | .name = name, | |
| 145 | .kind = entry_kind, | |
| 146 | }; | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | fn nextBsd(self: *Self) !?Entry { | |
| 151 | start_over: while (true) { | |
| 152 | if (self.index >= self.end_index) { | |
| 153 | if (self.first_iter) { | |
| 154 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 155 | self.first_iter = false; | |
| 156 | } | |
| 157 | const rc = if (builtin.os.tag == .netbsd) | |
| 158 | posix.system.__getdents30(self.dir.fd, &self.buf, self.buf.len) | |
| 159 | else | |
| 160 | posix.system.getdents(self.dir.fd, &self.buf, self.buf.len); | |
| 161 | switch (posix.errno(rc)) { | |
| 162 | .SUCCESS => {}, | |
| 163 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 164 | .FAULT => unreachable, | |
| 165 | .NOTDIR => unreachable, | |
| 166 | .INVAL => unreachable, | |
| 167 | // Introduced in freebsd 13.2: directory unlinked but still open. | |
| 168 | // To be consistent, iteration ends if the directory being iterated is deleted during iteration. | |
| 169 | .NOENT => return null, | |
| 170 | else => |err| return posix.unexpectedErrno(err), | |
| 171 | } | |
| 172 | if (rc == 0) return null; | |
| 173 | self.index = 0; | |
| 174 | self.end_index = @as(usize, @intCast(rc)); | |
| 175 | } | |
| 176 | const bsd_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 177 | const next_index = self.index + bsd_entry.reclen(); | |
| 178 | self.index = next_index; | |
| 179 | ||
| 180 | const name = @as([*]u8, @ptrCast(&bsd_entry.d_name))[0..bsd_entry.d_namlen]; | |
| 181 | ||
| 182 | const skip_zero_fileno = switch (builtin.os.tag) { | |
| 183 | // d_fileno=0 is used to mark invalid entries or deleted files. | |
| 184 | .openbsd, .netbsd => true, | |
| 185 | else => false, | |
| 186 | }; | |
| 187 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or | |
| 188 | (skip_zero_fileno and bsd_entry.d_fileno == 0)) | |
| 189 | { | |
| 190 | continue :start_over; | |
| 191 | } | |
| 192 | ||
| 193 | const entry_kind: Entry.Kind = switch (bsd_entry.d_type) { | |
| 194 | posix.DT.BLK => .block_device, | |
| 195 | posix.DT.CHR => .character_device, | |
| 196 | posix.DT.DIR => .directory, | |
| 197 | posix.DT.FIFO => .named_pipe, | |
| 198 | posix.DT.LNK => .sym_link, | |
| 199 | posix.DT.REG => .file, | |
| 200 | posix.DT.SOCK => .unix_domain_socket, | |
| 201 | posix.DT.WHT => .whiteout, | |
| 202 | else => .unknown, | |
| 203 | }; | |
| 204 | return Entry{ | |
| 205 | .name = name, | |
| 206 | .kind = entry_kind, | |
| 207 | }; | |
| 208 | } | |
| 209 | } | |
| 210 | ||
| 211 | pub fn reset(self: *Self) void { | |
| 212 | self.index = 0; | |
| 213 | self.end_index = 0; | |
| 214 | self.first_iter = true; | |
| 215 | } | |
| 216 | }, | |
| 217 | .haiku => struct { | |
| 218 | dir: Dir, | |
| 219 | buf: [1024]u8, // TODO align(@alignOf(posix.dirent64)), | |
| 220 | index: usize, | |
| 221 | end_index: usize, | |
| 222 | first_iter: bool, | |
| 223 | ||
| 224 | const Self = @This(); | |
| 225 | ||
| 226 | pub const Error = IteratorError; | |
| 227 | ||
| 228 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 229 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 230 | pub fn next(self: *Self) Error!?Entry { | |
| 231 | start_over: while (true) { | |
| 232 | // TODO: find a better max | |
| 233 | const HAIKU_MAX_COUNT = 10000; | |
| 234 | if (self.index >= self.end_index) { | |
| 235 | if (self.first_iter) { | |
| 236 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 237 | self.first_iter = false; | |
| 238 | } | |
| 239 | const rc = posix.system._kern_read_dir( | |
| 240 | self.dir.fd, | |
| 241 | &self.buf, | |
| 242 | self.buf.len, | |
| 243 | HAIKU_MAX_COUNT, | |
| 244 | ); | |
| 245 | if (rc == 0) return null; | |
| 246 | if (rc < 0) { | |
| 247 | switch (posix.errno(rc)) { | |
| 248 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 249 | .FAULT => unreachable, | |
| 250 | .NOTDIR => unreachable, | |
| 251 | .INVAL => unreachable, | |
| 252 | else => |err| return posix.unexpectedErrno(err), | |
| 253 | } | |
| 254 | } | |
| 255 | self.index = 0; | |
| 256 | self.end_index = @as(usize, @intCast(rc)); | |
| 257 | } | |
| 258 | const haiku_entry = @as(*align(1) posix.system.dirent, @ptrCast(&self.buf[self.index])); | |
| 259 | const next_index = self.index + haiku_entry.reclen(); | |
| 260 | self.index = next_index; | |
| 261 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&haiku_entry.d_name)), 0); | |
| 262 | ||
| 263 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..") or (haiku_entry.d_ino == 0)) { | |
| 264 | continue :start_over; | |
| 265 | } | |
| 266 | ||
| 267 | var stat_info: posix.Stat = undefined; | |
| 268 | const rc = posix.system._kern_read_stat( | |
| 269 | self.dir.fd, | |
| 270 | &haiku_entry.d_name, | |
| 271 | false, | |
| 272 | &stat_info, | |
| 273 | 0, | |
| 274 | ); | |
| 275 | if (rc != 0) { | |
| 276 | switch (posix.errno(rc)) { | |
| 277 | .SUCCESS => {}, | |
| 278 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 279 | .FAULT => unreachable, | |
| 280 | .NOTDIR => unreachable, | |
| 281 | .INVAL => unreachable, | |
| 282 | else => |err| return posix.unexpectedErrno(err), | |
| 283 | } | |
| 284 | } | |
| 285 | const statmode = stat_info.mode & posix.S.IFMT; | |
| 286 | ||
| 287 | const entry_kind: Entry.Kind = switch (statmode) { | |
| 288 | posix.S.IFDIR => .directory, | |
| 289 | posix.S.IFBLK => .block_device, | |
| 290 | posix.S.IFCHR => .character_device, | |
| 291 | posix.S.IFLNK => .sym_link, | |
| 292 | posix.S.IFREG => .file, | |
| 293 | posix.S.IFIFO => .named_pipe, | |
| 294 | else => .unknown, | |
| 295 | }; | |
| 296 | ||
| 297 | return Entry{ | |
| 298 | .name = name, | |
| 299 | .kind = entry_kind, | |
| 300 | }; | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 | pub fn reset(self: *Self) void { | |
| 305 | self.index = 0; | |
| 306 | self.end_index = 0; | |
| 307 | self.first_iter = true; | |
| 308 | } | |
| 309 | }, | |
| 310 | .linux => struct { | |
| 311 | dir: Dir, | |
| 312 | // The if guard is solely there to prevent compile errors from missing `linux.dirent64` | |
| 313 | // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. | |
| 314 | buf: [1024]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), | |
| 315 | index: usize, | |
| 316 | end_index: usize, | |
| 317 | first_iter: bool, | |
| 318 | ||
| 319 | const Self = @This(); | |
| 320 | const linux = std.os.linux; | |
| 321 | ||
| 322 | pub const Error = IteratorError; | |
| 323 | ||
| 324 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 325 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 326 | pub fn next(self: *Self) Error!?Entry { | |
| 327 | return self.nextLinux() catch |err| switch (err) { | |
| 328 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 329 | // This matches the behavior of non-Linux UNIX platforms. | |
| 330 | error.DirNotFound => null, | |
| 331 | else => |e| return e, | |
| 332 | }; | |
| 333 | } | |
| 334 | ||
| 335 | pub const ErrorLinux = error{DirNotFound} || IteratorError; | |
| 336 | ||
| 337 | /// Implementation of `next` that can return `error.DirNotFound` if the directory being | |
| 338 | /// iterated was deleted during iteration (this error is Linux specific). | |
| 339 | pub fn nextLinux(self: *Self) ErrorLinux!?Entry { | |
| 340 | start_over: while (true) { | |
| 341 | if (self.index >= self.end_index) { | |
| 342 | if (self.first_iter) { | |
| 343 | posix.lseek_SET(self.dir.fd, 0) catch unreachable; // EBADF here likely means that the Dir was not opened with iteration permissions | |
| 344 | self.first_iter = false; | |
| 345 | } | |
| 346 | const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); | |
| 347 | switch (linux.getErrno(rc)) { | |
| 348 | .SUCCESS => {}, | |
| 349 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 350 | .FAULT => unreachable, | |
| 351 | .NOTDIR => unreachable, | |
| 352 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 353 | .INVAL => return error.Unexpected, // Linux may in some cases return EINVAL when reading /proc/$PID/net. | |
| 354 | .ACCES => return error.AccessDenied, // Do not have permission to iterate this directory. | |
| 355 | else => |err| return posix.unexpectedErrno(err), | |
| 356 | } | |
| 357 | if (rc == 0) return null; | |
| 358 | self.index = 0; | |
| 359 | self.end_index = rc; | |
| 360 | } | |
| 361 | const linux_entry = @as(*align(1) linux.dirent64, @ptrCast(&self.buf[self.index])); | |
| 362 | const next_index = self.index + linux_entry.reclen(); | |
| 363 | self.index = next_index; | |
| 364 | ||
| 365 | const name = mem.sliceTo(@as([*:0]u8, @ptrCast(&linux_entry.d_name)), 0); | |
| 366 | ||
| 367 | // skip . and .. entries | |
| 368 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 369 | continue :start_over; | |
| 370 | } | |
| 371 | ||
| 372 | const entry_kind: Entry.Kind = switch (linux_entry.d_type) { | |
| 373 | linux.DT.BLK => .block_device, | |
| 374 | linux.DT.CHR => .character_device, | |
| 375 | linux.DT.DIR => .directory, | |
| 376 | linux.DT.FIFO => .named_pipe, | |
| 377 | linux.DT.LNK => .sym_link, | |
| 378 | linux.DT.REG => .file, | |
| 379 | linux.DT.SOCK => .unix_domain_socket, | |
| 380 | else => .unknown, | |
| 381 | }; | |
| 382 | return Entry{ | |
| 383 | .name = name, | |
| 384 | .kind = entry_kind, | |
| 385 | }; | |
| 386 | } | |
| 387 | } | |
| 388 | ||
| 389 | pub fn reset(self: *Self) void { | |
| 390 | self.index = 0; | |
| 391 | self.end_index = 0; | |
| 392 | self.first_iter = true; | |
| 393 | } | |
| 394 | }, | |
| 395 | .windows => struct { | |
| 396 | dir: Dir, | |
| 397 | buf: [1024]u8 align(@alignOf(std.os.windows.FILE_BOTH_DIR_INFORMATION)), | |
| 398 | index: usize, | |
| 399 | end_index: usize, | |
| 400 | first_iter: bool, | |
| 401 | name_data: [fs.MAX_NAME_BYTES]u8, | |
| 402 | ||
| 403 | const Self = @This(); | |
| 404 | ||
| 405 | pub const Error = IteratorError; | |
| 406 | ||
| 407 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 408 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 409 | pub fn next(self: *Self) Error!?Entry { | |
| 410 | while (true) { | |
| 411 | const w = std.os.windows; | |
| 412 | if (self.index >= self.end_index) { | |
| 413 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 414 | const rc = w.ntdll.NtQueryDirectoryFile( | |
| 415 | self.dir.fd, | |
| 416 | null, | |
| 417 | null, | |
| 418 | null, | |
| 419 | &io, | |
| 420 | &self.buf, | |
| 421 | self.buf.len, | |
| 422 | .FileBothDirectoryInformation, | |
| 423 | w.FALSE, | |
| 424 | null, | |
| 425 | if (self.first_iter) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), | |
| 426 | ); | |
| 427 | self.first_iter = false; | |
| 428 | if (io.Information == 0) return null; | |
| 429 | self.index = 0; | |
| 430 | self.end_index = io.Information; | |
| 431 | switch (rc) { | |
| 432 | .SUCCESS => {}, | |
| 433 | .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability | |
| 434 | ||
| 435 | else => return w.unexpectedStatus(rc), | |
| 436 | } | |
| 437 | } | |
| 438 | ||
| 439 | // While the official api docs guarantee FILE_BOTH_DIR_INFORMATION to be aligned properly | |
| 440 | // this may not always be the case (e.g. due to faulty VM/Sandboxing tools) | |
| 441 | const dir_info: *align(2) w.FILE_BOTH_DIR_INFORMATION = @ptrCast(@alignCast(&self.buf[self.index])); | |
| 442 | if (dir_info.NextEntryOffset != 0) { | |
| 443 | self.index += dir_info.NextEntryOffset; | |
| 444 | } else { | |
| 445 | self.index = self.buf.len; | |
| 446 | } | |
| 447 | ||
| 448 | const name_utf16le = @as([*]u16, @ptrCast(&dir_info.FileName))[0 .. dir_info.FileNameLength / 2]; | |
| 449 | ||
| 450 | if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) | |
| 451 | continue; | |
| 452 | // Trust that Windows gives us valid UTF-16LE | |
| 453 | const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; | |
| 454 | const name_utf8 = self.name_data[0..name_utf8_len]; | |
| 455 | const kind: Entry.Kind = blk: { | |
| 456 | const attrs = dir_info.FileAttributes; | |
| 457 | if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk .directory; | |
| 458 | if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk .sym_link; | |
| 459 | break :blk .file; | |
| 460 | }; | |
| 461 | return Entry{ | |
| 462 | .name = name_utf8, | |
| 463 | .kind = kind, | |
| 464 | }; | |
| 465 | } | |
| 466 | } | |
| 467 | ||
| 468 | pub fn reset(self: *Self) void { | |
| 469 | self.index = 0; | |
| 470 | self.end_index = 0; | |
| 471 | self.first_iter = true; | |
| 472 | } | |
| 473 | }, | |
| 474 | .wasi => struct { | |
| 475 | dir: Dir, | |
| 476 | buf: [1024]u8, // TODO align(@alignOf(posix.wasi.dirent_t)), | |
| 477 | cookie: u64, | |
| 478 | index: usize, | |
| 479 | end_index: usize, | |
| 480 | ||
| 481 | const Self = @This(); | |
| 482 | ||
| 483 | pub const Error = IteratorError; | |
| 484 | ||
| 485 | /// Memory such as file names referenced in this returned entry becomes invalid | |
| 486 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. | |
| 487 | pub fn next(self: *Self) Error!?Entry { | |
| 488 | return self.nextWasi() catch |err| switch (err) { | |
| 489 | // To be consistent across platforms, iteration ends if the directory being iterated is deleted during iteration. | |
| 490 | // This matches the behavior of non-Linux UNIX platforms. | |
| 491 | error.DirNotFound => null, | |
| 492 | else => |e| return e, | |
| 493 | }; | |
| 494 | } | |
| 495 | ||
| 496 | pub const ErrorWasi = error{DirNotFound} || IteratorError; | |
| 497 | ||
| 498 | /// Implementation of `next` that can return platform-dependent errors depending on the host platform. | |
| 499 | /// When the host platform is Linux, `error.DirNotFound` can be returned if the directory being | |
| 500 | /// iterated was deleted during iteration. | |
| 501 | pub fn nextWasi(self: *Self) ErrorWasi!?Entry { | |
| 502 | // We intentinally use fd_readdir even when linked with libc, | |
| 503 | // since its implementation is exactly the same as below, | |
| 504 | // and we avoid the code complexity here. | |
| 505 | const w = std.os.wasi; | |
| 506 | start_over: while (true) { | |
| 507 | // According to the WASI spec, the last entry might be truncated, | |
| 508 | // so we need to check if the left buffer contains the whole dirent. | |
| 509 | if (self.end_index - self.index < @sizeOf(w.dirent_t)) { | |
| 510 | var bufused: usize = undefined; | |
| 511 | switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { | |
| 512 | .SUCCESS => {}, | |
| 513 | .BADF => unreachable, // Dir is invalid or was opened without iteration ability | |
| 514 | .FAULT => unreachable, | |
| 515 | .NOTDIR => unreachable, | |
| 516 | .INVAL => unreachable, | |
| 517 | .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. | |
| 518 | .NOTCAPABLE => return error.AccessDenied, | |
| 519 | else => |err| return posix.unexpectedErrno(err), | |
| 520 | } | |
| 521 | if (bufused == 0) return null; | |
| 522 | self.index = 0; | |
| 523 | self.end_index = bufused; | |
| 524 | } | |
| 525 | const entry = @as(*align(1) w.dirent_t, @ptrCast(&self.buf[self.index])); | |
| 526 | const entry_size = @sizeOf(w.dirent_t); | |
| 527 | const name_index = self.index + entry_size; | |
| 528 | if (name_index + entry.d_namlen > self.end_index) { | |
| 529 | // This case, the name is truncated, so we need to call readdir to store the entire name. | |
| 530 | self.end_index = self.index; // Force fd_readdir in the next loop. | |
| 531 | continue :start_over; | |
| 532 | } | |
| 533 | const name = self.buf[name_index .. name_index + entry.d_namlen]; | |
| 534 | ||
| 535 | const next_index = name_index + entry.d_namlen; | |
| 536 | self.index = next_index; | |
| 537 | self.cookie = entry.d_next; | |
| 538 | ||
| 539 | // skip . and .. entries | |
| 540 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { | |
| 541 | continue :start_over; | |
| 542 | } | |
| 543 | ||
| 544 | const entry_kind: Entry.Kind = switch (entry.d_type) { | |
| 545 | .BLOCK_DEVICE => .block_device, | |
| 546 | .CHARACTER_DEVICE => .character_device, | |
| 547 | .DIRECTORY => .directory, | |
| 548 | .SYMBOLIC_LINK => .sym_link, | |
| 549 | .REGULAR_FILE => .file, | |
| 550 | .SOCKET_STREAM, .SOCKET_DGRAM => .unix_domain_socket, | |
| 551 | else => .unknown, | |
| 552 | }; | |
| 553 | return Entry{ | |
| 554 | .name = name, | |
| 555 | .kind = entry_kind, | |
| 556 | }; | |
| 557 | } | |
| 558 | } | |
| 559 | ||
| 560 | pub fn reset(self: *Self) void { | |
| 561 | self.index = 0; | |
| 562 | self.end_index = 0; | |
| 563 | self.cookie = std.os.wasi.DIRCOOKIE_START; | |
| 564 | } | |
| 565 | }, | |
| 566 | else => @compileError("unimplemented"), | |
| 567 | }; | |
| 568 | ||
| 569 | pub fn iterate(self: Dir) Iterator { | |
| 570 | return self.iterateImpl(true); | |
| 571 | } | |
| 572 | ||
| 573 | /// Like `iterate`, but will not reset the directory cursor before the first | |
| 574 | /// iteration. This should only be used in cases where it is known that the | |
| 575 | /// `Dir` has not had its cursor modified yet (e.g. it was just opened). | |
| 576 | pub fn iterateAssumeFirstIteration(self: Dir) Iterator { | |
| 577 | return self.iterateImpl(false); | |
| 578 | } | |
| 579 | ||
| 580 | fn iterateImpl(self: Dir, first_iter_start_value: bool) Iterator { | |
| 581 | switch (builtin.os.tag) { | |
| 582 | .macos, | |
| 583 | .ios, | |
| 584 | .freebsd, | |
| 585 | .netbsd, | |
| 586 | .dragonfly, | |
| 587 | .openbsd, | |
| 588 | .solaris, | |
| 589 | .illumos, | |
| 590 | => return Iterator{ | |
| 591 | .dir = self, | |
| 592 | .seek = 0, | |
| 593 | .index = 0, | |
| 594 | .end_index = 0, | |
| 595 | .buf = undefined, | |
| 596 | .first_iter = first_iter_start_value, | |
| 597 | }, | |
| 598 | .linux, .haiku => return Iterator{ | |
| 599 | .dir = self, | |
| 600 | .index = 0, | |
| 601 | .end_index = 0, | |
| 602 | .buf = undefined, | |
| 603 | .first_iter = first_iter_start_value, | |
| 604 | }, | |
| 605 | .windows => return Iterator{ | |
| 606 | .dir = self, | |
| 607 | .index = 0, | |
| 608 | .end_index = 0, | |
| 609 | .first_iter = first_iter_start_value, | |
| 610 | .buf = undefined, | |
| 611 | .name_data = undefined, | |
| 612 | }, | |
| 613 | .wasi => return Iterator{ | |
| 614 | .dir = self, | |
| 615 | .cookie = std.os.wasi.DIRCOOKIE_START, | |
| 616 | .index = 0, | |
| 617 | .end_index = 0, | |
| 618 | .buf = undefined, | |
| 619 | }, | |
| 620 | else => @compileError("unimplemented"), | |
| 621 | } | |
| 622 | } | |
| 623 | ||
| 624 | pub const Walker = struct { | |
| 625 | stack: std.ArrayList(StackItem), | |
| 626 | name_buffer: std.ArrayList(u8), | |
| 627 | ||
| 628 | pub const WalkerEntry = struct { | |
| 629 | /// The containing directory. This can be used to operate directly on `basename` | |
| 630 | /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. | |
| 631 | /// The directory remains open until `next` or `deinit` is called. | |
| 632 | dir: Dir, | |
| 633 | basename: []const u8, | |
| 634 | path: []const u8, | |
| 635 | kind: Dir.Entry.Kind, | |
| 636 | }; | |
| 637 | ||
| 638 | const StackItem = struct { | |
| 639 | iter: Dir.Iterator, | |
| 640 | dirname_len: usize, | |
| 641 | }; | |
| 642 | ||
| 643 | /// After each call to this function, and on deinit(), the memory returned | |
| 644 | /// from this function becomes invalid. A copy must be made in order to keep | |
| 645 | /// a reference to the path. | |
| 646 | pub fn next(self: *Walker) !?WalkerEntry { | |
| 647 | while (self.stack.items.len != 0) { | |
| 648 | // `top` and `containing` become invalid after appending to `self.stack` | |
| 649 | var top = &self.stack.items[self.stack.items.len - 1]; | |
| 650 | var containing = top; | |
| 651 | var dirname_len = top.dirname_len; | |
| 652 | if (top.iter.next() catch |err| { | |
| 653 | // If we get an error, then we want the user to be able to continue | |
| 654 | // walking if they want, which means that we need to pop the directory | |
| 655 | // that errored from the stack. Otherwise, all future `next` calls would | |
| 656 | // likely just fail with the same error. | |
| 657 | var item = self.stack.pop(); | |
| 658 | if (self.stack.items.len != 0) { | |
| 659 | item.iter.dir.close(); | |
| 660 | } | |
| 661 | return err; | |
| 662 | }) |base| { | |
| 663 | self.name_buffer.shrinkRetainingCapacity(dirname_len); | |
| 664 | if (self.name_buffer.items.len != 0) { | |
| 665 | try self.name_buffer.append(fs.path.sep); | |
| 666 | dirname_len += 1; | |
| 667 | } | |
| 668 | try self.name_buffer.appendSlice(base.name); | |
| 669 | if (base.kind == .directory) { | |
| 670 | var new_dir = top.iter.dir.openDir(base.name, .{ .iterate = true }) catch |err| switch (err) { | |
| 671 | error.NameTooLong => unreachable, // no path sep in base.name | |
| 672 | else => |e| return e, | |
| 673 | }; | |
| 674 | { | |
| 675 | errdefer new_dir.close(); | |
| 676 | try self.stack.append(StackItem{ | |
| 677 | .iter = new_dir.iterateAssumeFirstIteration(), | |
| 678 | .dirname_len = self.name_buffer.items.len, | |
| 679 | }); | |
| 680 | top = &self.stack.items[self.stack.items.len - 1]; | |
| 681 | containing = &self.stack.items[self.stack.items.len - 2]; | |
| 682 | } | |
| 683 | } | |
| 684 | return WalkerEntry{ | |
| 685 | .dir = containing.iter.dir, | |
| 686 | .basename = self.name_buffer.items[dirname_len..], | |
| 687 | .path = self.name_buffer.items, | |
| 688 | .kind = base.kind, | |
| 689 | }; | |
| 690 | } else { | |
| 691 | var item = self.stack.pop(); | |
| 692 | if (self.stack.items.len != 0) { | |
| 693 | item.iter.dir.close(); | |
| 694 | } | |
| 695 | } | |
| 696 | } | |
| 697 | return null; | |
| 698 | } | |
| 699 | ||
| 700 | pub fn deinit(self: *Walker) void { | |
| 701 | // Close any remaining directories except the initial one (which is always at index 0) | |
| 702 | if (self.stack.items.len > 1) { | |
| 703 | for (self.stack.items[1..]) |*item| { | |
| 704 | item.iter.dir.close(); | |
| 705 | } | |
| 706 | } | |
| 707 | self.stack.deinit(); | |
| 708 | self.name_buffer.deinit(); | |
| 709 | } | |
| 710 | }; | |
| 711 | ||
| 712 | /// Recursively iterates over a directory. | |
| 713 | /// `self` must have been opened with `OpenDirOptions{.iterate = true}`. | |
| 714 | /// Must call `Walker.deinit` when done. | |
| 715 | /// The order of returned file system entries is undefined. | |
| 716 | /// `self` will not be closed after walking it. | |
| 717 | pub fn walk(self: Dir, allocator: Allocator) !Walker { | |
| 718 | var name_buffer = std.ArrayList(u8).init(allocator); | |
| 719 | errdefer name_buffer.deinit(); | |
| 720 | ||
| 721 | var stack = std.ArrayList(Walker.StackItem).init(allocator); | |
| 722 | errdefer stack.deinit(); | |
| 723 | ||
| 724 | try stack.append(Walker.StackItem{ | |
| 725 | .iter = self.iterate(), | |
| 726 | .dirname_len = 0, | |
| 727 | }); | |
| 728 | ||
| 729 | return Walker{ | |
| 730 | .stack = stack, | |
| 731 | .name_buffer = name_buffer, | |
| 732 | }; | |
| 733 | } | |
| 734 | ||
| 735 | pub const OpenError = error{ | |
| 736 | FileNotFound, | |
| 737 | NotDir, | |
| 738 | InvalidHandle, | |
| 739 | AccessDenied, | |
| 740 | SymLinkLoop, | |
| 741 | ProcessFdQuotaExceeded, | |
| 742 | NameTooLong, | |
| 743 | SystemFdQuotaExceeded, | |
| 744 | NoDevice, | |
| 745 | SystemResources, | |
| 746 | InvalidUtf8, | |
| 747 | BadPathName, | |
| 748 | DeviceBusy, | |
| 749 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 750 | NetworkNotFound, | |
| 751 | } || posix.UnexpectedError; | |
| 752 | ||
| 753 | pub fn close(self: *Dir) void { | |
| 754 | if (fs.need_async_thread) { | |
| 755 | std.event.Loop.instance.?.close(self.fd); | |
| 756 | } else { | |
| 757 | posix.close(self.fd); | |
| 758 | } | |
| 759 | self.* = undefined; | |
| 760 | } | |
| 761 | ||
| 762 | /// Opens a file for reading or writing, without attempting to create a new file. | |
| 763 | /// To create a new file, see `createFile`. | |
| 764 | /// Call `File.close` to release the resource. | |
| 765 | /// Asserts that the path parameter has no null bytes. | |
| 766 | pub fn openFile(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 767 | if (builtin.os.tag == .windows) { | |
| 768 | const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 769 | return self.openFileW(path_w.span(), flags); | |
| 770 | } | |
| 771 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 772 | return self.openFileWasi(sub_path, flags); | |
| 773 | } | |
| 774 | const path_c = try posix.toPosixPath(sub_path); | |
| 775 | return self.openFileZ(&path_c, flags); | |
| 776 | } | |
| 777 | ||
| 778 | /// Same as `openFile` but WASI only. | |
| 779 | pub fn openFileWasi(self: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 780 | const w = std.os.wasi; | |
| 781 | var fdflags: w.fdflags_t = 0x0; | |
| 782 | var base: w.rights_t = 0x0; | |
| 783 | if (flags.isRead()) { | |
| 784 | base |= w.RIGHT.FD_READ | w.RIGHT.FD_TELL | w.RIGHT.FD_SEEK | w.RIGHT.FD_FILESTAT_GET; | |
| 785 | } | |
| 786 | if (flags.isWrite()) { | |
| 787 | fdflags |= w.FDFLAG.APPEND; | |
| 788 | base |= w.RIGHT.FD_WRITE | | |
| 789 | w.RIGHT.FD_TELL | | |
| 790 | w.RIGHT.FD_SEEK | | |
| 791 | w.RIGHT.FD_DATASYNC | | |
| 792 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 793 | w.RIGHT.FD_SYNC | | |
| 794 | w.RIGHT.FD_ALLOCATE | | |
| 795 | w.RIGHT.FD_ADVISE | | |
| 796 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 797 | w.RIGHT.FD_FILESTAT_SET_SIZE; | |
| 798 | } | |
| 799 | const fd = try posix.openatWasi(self.fd, sub_path, 0x0, 0x0, fdflags, base, 0x0); | |
| 800 | return File{ .handle = fd }; | |
| 801 | } | |
| 802 | ||
| 803 | /// Same as `openFile` but the path parameter is null-terminated. | |
| 804 | pub fn openFileZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) File.OpenError!File { | |
| 805 | if (builtin.os.tag == .windows) { | |
| 806 | const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path); | |
| 807 | return self.openFileW(path_w.span(), flags); | |
| 808 | } | |
| 809 | ||
| 810 | var os_flags: u32 = 0; | |
| 811 | if (@hasDecl(posix.O, "CLOEXEC")) os_flags = posix.O.CLOEXEC; | |
| 812 | ||
| 813 | // Use the O locking flags if the os supports them to acquire the lock | |
| 814 | // atomically. | |
| 815 | const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); | |
| 816 | if (has_flock_open_flags) { | |
| 817 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 818 | // is successful. | |
| 819 | const nonblocking_lock_flag: u32 = if (flags.lock_nonblocking) | |
| 820 | posix.O.NONBLOCK | |
| 821 | else | |
| 822 | 0; | |
| 823 | os_flags |= switch (flags.lock) { | |
| 824 | .none => @as(u32, 0), | |
| 825 | .shared => posix.O.SHLOCK | nonblocking_lock_flag, | |
| 826 | .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, | |
| 827 | }; | |
| 828 | } | |
| 829 | if (@hasDecl(posix.O, "LARGEFILE")) { | |
| 830 | os_flags |= posix.O.LARGEFILE; | |
| 831 | } | |
| 832 | if (@hasDecl(posix.O, "NOCTTY") and !flags.allow_ctty) { | |
| 833 | os_flags |= posix.O.NOCTTY; | |
| 834 | } | |
| 835 | os_flags |= switch (flags.mode) { | |
| 836 | .read_only => @as(u32, posix.O.RDONLY), | |
| 837 | .write_only => @as(u32, posix.O.WRONLY), | |
| 838 | .read_write => @as(u32, posix.O.RDWR), | |
| 839 | }; | |
| 840 | const fd = if (flags.intended_io_mode != .blocking) | |
| 841 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path, os_flags, 0) | |
| 842 | else | |
| 843 | try posix.openatZ(self.fd, sub_path, os_flags, 0); | |
| 844 | errdefer posix.close(fd); | |
| 845 | ||
| 846 | // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block | |
| 847 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 848 | if (@hasDecl(posix.system, "LOCK") and builtin.target.os.tag != .wasi) { | |
| 849 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 850 | // TODO: integrate async I/O | |
| 851 | const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; | |
| 852 | try posix.flock(fd, switch (flags.lock) { | |
| 853 | .none => unreachable, | |
| 854 | .shared => posix.LOCK.SH | lock_nonblocking, | |
| 855 | .exclusive => posix.LOCK.EX | lock_nonblocking, | |
| 856 | }); | |
| 857 | } | |
| 858 | } | |
| 859 | ||
| 860 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 861 | var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { | |
| 862 | error.FileBusy => unreachable, | |
| 863 | error.Locked => unreachable, | |
| 864 | error.PermissionDenied => unreachable, | |
| 865 | error.DeadLock => unreachable, | |
| 866 | error.LockedRegionLimitExceeded => unreachable, | |
| 867 | else => |e| return e, | |
| 868 | }; | |
| 869 | fl_flags &= ~@as(usize, posix.O.NONBLOCK); | |
| 870 | _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 871 | error.FileBusy => unreachable, | |
| 872 | error.Locked => unreachable, | |
| 873 | error.PermissionDenied => unreachable, | |
| 874 | error.DeadLock => unreachable, | |
| 875 | error.LockedRegionLimitExceeded => unreachable, | |
| 876 | else => |e| return e, | |
| 877 | }; | |
| 878 | } | |
| 879 | ||
| 880 | return File{ | |
| 881 | .handle = fd, | |
| 882 | .capable_io_mode = .blocking, | |
| 883 | .intended_io_mode = flags.intended_io_mode, | |
| 884 | }; | |
| 885 | } | |
| 886 | ||
| 887 | /// Same as `openFile` but Windows-only and the path parameter is | |
| 888 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 889 | pub fn openFileW(self: Dir, sub_path_w: []const u16, flags: File.OpenFlags) File.OpenError!File { | |
| 890 | const w = std.os.windows; | |
| 891 | const file: File = .{ | |
| 892 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 893 | .dir = self.fd, | |
| 894 | .access_mask = w.SYNCHRONIZE | | |
| 895 | (if (flags.isRead()) @as(u32, w.GENERIC_READ) else 0) | | |
| 896 | (if (flags.isWrite()) @as(u32, w.GENERIC_WRITE) else 0), | |
| 897 | .creation = w.FILE_OPEN, | |
| 898 | .io_mode = flags.intended_io_mode, | |
| 899 | }), | |
| 900 | .capable_io_mode = std.io.default_mode, | |
| 901 | .intended_io_mode = flags.intended_io_mode, | |
| 902 | }; | |
| 903 | errdefer file.close(); | |
| 904 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 905 | const range_off: w.LARGE_INTEGER = 0; | |
| 906 | const range_len: w.LARGE_INTEGER = 1; | |
| 907 | const exclusive = switch (flags.lock) { | |
| 908 | .none => return file, | |
| 909 | .shared => false, | |
| 910 | .exclusive => true, | |
| 911 | }; | |
| 912 | try w.LockFile( | |
| 913 | file.handle, | |
| 914 | null, | |
| 915 | null, | |
| 916 | null, | |
| 917 | &io, | |
| 918 | &range_off, | |
| 919 | &range_len, | |
| 920 | null, | |
| 921 | @intFromBool(flags.lock_nonblocking), | |
| 922 | @intFromBool(exclusive), | |
| 923 | ); | |
| 924 | return file; | |
| 925 | } | |
| 926 | ||
| 927 | /// Creates, opens, or overwrites a file with write access. | |
| 928 | /// Call `File.close` on the result when done. | |
| 929 | /// Asserts that the path parameter has no null bytes. | |
| 930 | pub fn createFile(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 931 | if (builtin.os.tag == .windows) { | |
| 932 | const path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 933 | return self.createFileW(path_w.span(), flags); | |
| 934 | } | |
| 935 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 936 | return self.createFileWasi(sub_path, flags); | |
| 937 | } | |
| 938 | const path_c = try posix.toPosixPath(sub_path); | |
| 939 | return self.createFileZ(&path_c, flags); | |
| 940 | } | |
| 941 | ||
| 942 | /// Same as `createFile` but WASI only. | |
| 943 | pub fn createFileWasi(self: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 944 | const w = std.os.wasi; | |
| 945 | var oflags = w.O.CREAT; | |
| 946 | var base: w.rights_t = w.RIGHT.FD_WRITE | | |
| 947 | w.RIGHT.FD_DATASYNC | | |
| 948 | w.RIGHT.FD_SEEK | | |
| 949 | w.RIGHT.FD_TELL | | |
| 950 | w.RIGHT.FD_FDSTAT_SET_FLAGS | | |
| 951 | w.RIGHT.FD_SYNC | | |
| 952 | w.RIGHT.FD_ALLOCATE | | |
| 953 | w.RIGHT.FD_ADVISE | | |
| 954 | w.RIGHT.FD_FILESTAT_SET_TIMES | | |
| 955 | w.RIGHT.FD_FILESTAT_SET_SIZE | | |
| 956 | w.RIGHT.FD_FILESTAT_GET; | |
| 957 | if (flags.read) { | |
| 958 | base |= w.RIGHT.FD_READ; | |
| 959 | } | |
| 960 | if (flags.truncate) { | |
| 961 | oflags |= w.O.TRUNC; | |
| 962 | } | |
| 963 | if (flags.exclusive) { | |
| 964 | oflags |= w.O.EXCL; | |
| 965 | } | |
| 966 | const fd = try posix.openatWasi(self.fd, sub_path, 0x0, oflags, 0x0, base, 0x0); | |
| 967 | return File{ .handle = fd }; | |
| 968 | } | |
| 969 | ||
| 970 | /// Same as `createFile` but the path parameter is null-terminated. | |
| 971 | pub fn createFileZ(self: Dir, sub_path_c: [*:0]const u8, flags: File.CreateFlags) File.OpenError!File { | |
| 972 | if (builtin.os.tag == .windows) { | |
| 973 | const path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 974 | return self.createFileW(path_w.span(), flags); | |
| 975 | } | |
| 976 | ||
| 977 | // Use the O locking flags if the os supports them to acquire the lock | |
| 978 | // atomically. | |
| 979 | const has_flock_open_flags = @hasDecl(posix.O, "EXLOCK"); | |
| 980 | // Note that the O.NONBLOCK flag is removed after the openat() call | |
| 981 | // is successful. | |
| 982 | const nonblocking_lock_flag: u32 = if (has_flock_open_flags and flags.lock_nonblocking) | |
| 983 | posix.O.NONBLOCK | |
| 984 | else | |
| 985 | 0; | |
| 986 | const lock_flag: u32 = if (has_flock_open_flags) switch (flags.lock) { | |
| 987 | .none => @as(u32, 0), | |
| 988 | .shared => posix.O.SHLOCK | nonblocking_lock_flag, | |
| 989 | .exclusive => posix.O.EXLOCK | nonblocking_lock_flag, | |
| 990 | } else 0; | |
| 991 | ||
| 992 | const O_LARGEFILE = if (@hasDecl(posix.O, "LARGEFILE")) posix.O.LARGEFILE else 0; | |
| 993 | const os_flags = lock_flag | O_LARGEFILE | posix.O.CREAT | posix.O.CLOEXEC | | |
| 994 | (if (flags.truncate) @as(u32, posix.O.TRUNC) else 0) | | |
| 995 | (if (flags.read) @as(u32, posix.O.RDWR) else posix.O.WRONLY) | | |
| 996 | (if (flags.exclusive) @as(u32, posix.O.EXCL) else 0); | |
| 997 | const fd = if (flags.intended_io_mode != .blocking) | |
| 998 | try std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, os_flags, flags.mode) | |
| 999 | else | |
| 1000 | try posix.openatZ(self.fd, sub_path_c, os_flags, flags.mode); | |
| 1001 | errdefer posix.close(fd); | |
| 1002 | ||
| 1003 | // WASI doesn't have posix.flock so we intetinally check OS prior to the inner if block | |
| 1004 | // since it is not compiltime-known and we need to avoid undefined symbol in Wasm. | |
| 1005 | if (builtin.target.os.tag != .wasi) { | |
| 1006 | if (!has_flock_open_flags and flags.lock != .none) { | |
| 1007 | // TODO: integrate async I/O | |
| 1008 | const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; | |
| 1009 | try posix.flock(fd, switch (flags.lock) { | |
| 1010 | .none => unreachable, | |
| 1011 | .shared => posix.LOCK.SH | lock_nonblocking, | |
| 1012 | .exclusive => posix.LOCK.EX | lock_nonblocking, | |
| 1013 | }); | |
| 1014 | } | |
| 1015 | } | |
| 1016 | ||
| 1017 | if (has_flock_open_flags and flags.lock_nonblocking) { | |
| 1018 | var fl_flags = posix.fcntl(fd, posix.F.GETFL, 0) catch |err| switch (err) { | |
| 1019 | error.FileBusy => unreachable, | |
| 1020 | error.Locked => unreachable, | |
| 1021 | error.PermissionDenied => unreachable, | |
| 1022 | error.DeadLock => unreachable, | |
| 1023 | error.LockedRegionLimitExceeded => unreachable, | |
| 1024 | else => |e| return e, | |
| 1025 | }; | |
| 1026 | fl_flags &= ~@as(usize, posix.O.NONBLOCK); | |
| 1027 | _ = posix.fcntl(fd, posix.F.SETFL, fl_flags) catch |err| switch (err) { | |
| 1028 | error.FileBusy => unreachable, | |
| 1029 | error.Locked => unreachable, | |
| 1030 | error.PermissionDenied => unreachable, | |
| 1031 | error.DeadLock => unreachable, | |
| 1032 | error.LockedRegionLimitExceeded => unreachable, | |
| 1033 | else => |e| return e, | |
| 1034 | }; | |
| 1035 | } | |
| 1036 | ||
| 1037 | return File{ | |
| 1038 | .handle = fd, | |
| 1039 | .capable_io_mode = .blocking, | |
| 1040 | .intended_io_mode = flags.intended_io_mode, | |
| 1041 | }; | |
| 1042 | } | |
| 1043 | ||
| 1044 | /// Same as `createFile` but Windows-only and the path parameter is | |
| 1045 | /// [WTF-16](https://simonsapin.github.io/wtf-8/#potentially-ill-formed-utf-16) encoded. | |
| 1046 | pub fn createFileW(self: Dir, sub_path_w: []const u16, flags: File.CreateFlags) File.OpenError!File { | |
| 1047 | const w = std.os.windows; | |
| 1048 | const read_flag = if (flags.read) @as(u32, w.GENERIC_READ) else 0; | |
| 1049 | const file: File = .{ | |
| 1050 | .handle = try w.OpenFile(sub_path_w, .{ | |
| 1051 | .dir = self.fd, | |
| 1052 | .access_mask = w.SYNCHRONIZE | w.GENERIC_WRITE | read_flag, | |
| 1053 | .creation = if (flags.exclusive) | |
| 1054 | @as(u32, w.FILE_CREATE) | |
| 1055 | else if (flags.truncate) | |
| 1056 | @as(u32, w.FILE_OVERWRITE_IF) | |
| 1057 | else | |
| 1058 | @as(u32, w.FILE_OPEN_IF), | |
| 1059 | .io_mode = flags.intended_io_mode, | |
| 1060 | }), | |
| 1061 | .capable_io_mode = std.io.default_mode, | |
| 1062 | .intended_io_mode = flags.intended_io_mode, | |
| 1063 | }; | |
| 1064 | errdefer file.close(); | |
| 1065 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1066 | const range_off: w.LARGE_INTEGER = 0; | |
| 1067 | const range_len: w.LARGE_INTEGER = 1; | |
| 1068 | const exclusive = switch (flags.lock) { | |
| 1069 | .none => return file, | |
| 1070 | .shared => false, | |
| 1071 | .exclusive => true, | |
| 1072 | }; | |
| 1073 | try w.LockFile( | |
| 1074 | file.handle, | |
| 1075 | null, | |
| 1076 | null, | |
| 1077 | null, | |
| 1078 | &io, | |
| 1079 | &range_off, | |
| 1080 | &range_len, | |
| 1081 | null, | |
| 1082 | @intFromBool(flags.lock_nonblocking), | |
| 1083 | @intFromBool(exclusive), | |
| 1084 | ); | |
| 1085 | return file; | |
| 1086 | } | |
| 1087 | ||
| 1088 | /// Creates a single directory with a relative or absolute path. | |
| 1089 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1090 | /// To operate on only absolute paths, see `makeDirAbsolute`. | |
| 1091 | pub fn makeDir(self: Dir, sub_path: []const u8) !void { | |
| 1092 | try posix.mkdirat(self.fd, sub_path, default_mode); | |
| 1093 | } | |
| 1094 | ||
| 1095 | /// Creates a single directory with a relative or absolute null-terminated UTF-8-encoded path. | |
| 1096 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1097 | /// To operate on only absolute paths, see `makeDirAbsoluteZ`. | |
| 1098 | pub fn makeDirZ(self: Dir, sub_path: [*:0]const u8) !void { | |
| 1099 | try posix.mkdiratZ(self.fd, sub_path, default_mode); | |
| 1100 | } | |
| 1101 | ||
| 1102 | /// Creates a single directory with a relative or absolute null-terminated WTF-16-encoded path. | |
| 1103 | /// To create multiple directories to make an entire path, see `makePath`. | |
| 1104 | /// To operate on only absolute paths, see `makeDirAbsoluteW`. | |
| 1105 | pub fn makeDirW(self: Dir, sub_path: [*:0]const u16) !void { | |
| 1106 | try posix.mkdiratW(self.fd, sub_path, default_mode); | |
| 1107 | } | |
| 1108 | ||
| 1109 | /// Calls makeDir iteratively to make an entire path | |
| 1110 | /// (i.e. creating any parent directories that do not exist). | |
| 1111 | /// Returns success if the path already exists and is a directory. | |
| 1112 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1113 | /// have been modified regardless. | |
| 1114 | pub fn makePath(self: Dir, sub_path: []const u8) !void { | |
| 1115 | var it = try fs.path.componentIterator(sub_path); | |
| 1116 | var component = it.last() orelse return; | |
| 1117 | while (true) { | |
| 1118 | self.makeDir(component.path) catch |err| switch (err) { | |
| 1119 | error.PathAlreadyExists => { | |
| 1120 | // TODO stat the file and return an error if it's not a directory | |
| 1121 | // this is important because otherwise a dangling symlink | |
| 1122 | // could cause an infinite loop | |
| 1123 | }, | |
| 1124 | error.FileNotFound => |e| { | |
| 1125 | component = it.previous() orelse return e; | |
| 1126 | continue; | |
| 1127 | }, | |
| 1128 | else => |e| return e, | |
| 1129 | }; | |
| 1130 | component = it.next() orelse return; | |
| 1131 | } | |
| 1132 | } | |
| 1133 | ||
| 1134 | /// Calls makeOpenDirAccessMaskW iteratively to make an entire path | |
| 1135 | /// (i.e. creating any parent directories that do not exist). | |
| 1136 | /// Opens the dir if the path already exists and is a directory. | |
| 1137 | /// This function is not atomic, and if it returns an error, the file system may | |
| 1138 | /// have been modified regardless. | |
| 1139 | fn makeOpenPathAccessMaskW(self: Dir, sub_path: []const u8, access_mask: u32, no_follow: bool) OpenError!Dir { | |
| 1140 | const w = std.os.windows; | |
| 1141 | var it = try fs.path.componentIterator(sub_path); | |
| 1142 | // If there are no components in the path, then create a dummy component with the full path. | |
| 1143 | var component = it.last() orelse fs.path.NativeUtf8ComponentIterator.Component{ | |
| 1144 | .name = "", | |
| 1145 | .path = sub_path, | |
| 1146 | }; | |
| 1147 | ||
| 1148 | while (true) { | |
| 1149 | const sub_path_w = try w.sliceToPrefixedFileW(self.fd, component.path); | |
| 1150 | const is_last = it.peekNext() == null; | |
| 1151 | var result = self.makeOpenDirAccessMaskW(sub_path_w.span().ptr, access_mask, .{ | |
| 1152 | .no_follow = no_follow, | |
| 1153 | .create_disposition = if (is_last) w.FILE_OPEN_IF else w.FILE_CREATE, | |
| 1154 | }) catch |err| switch (err) { | |
| 1155 | error.FileNotFound => |e| { | |
| 1156 | component = it.previous() orelse return e; | |
| 1157 | continue; | |
| 1158 | }, | |
| 1159 | else => |e| return e, | |
| 1160 | }; | |
| 1161 | ||
| 1162 | component = it.next() orelse return result; | |
| 1163 | // Don't leak the intermediate file handles | |
| 1164 | result.close(); | |
| 1165 | } | |
| 1166 | } | |
| 1167 | ||
| 1168 | /// This function performs `makePath`, followed by `openDir`. | |
| 1169 | /// If supported by the OS, this operation is atomic. It is not atomic on | |
| 1170 | /// all operating systems. | |
| 1171 | /// On Windows, this function performs `makeOpenPathAccessMaskW`. | |
| 1172 | pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenDirOptions) !Dir { | |
| 1173 | return switch (builtin.os.tag) { | |
| 1174 | .windows => { | |
| 1175 | const w = std.os.windows; | |
| 1176 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1177 | w.SYNCHRONIZE | w.FILE_TRAVERSE | | |
| 1178 | (if (open_dir_options.iterate) w.FILE_LIST_DIRECTORY else @as(u32, 0)); | |
| 1179 | ||
| 1180 | return self.makeOpenPathAccessMaskW(sub_path, base_flags, open_dir_options.no_follow); | |
| 1181 | }, | |
| 1182 | else => { | |
| 1183 | return self.openDir(sub_path, open_dir_options) catch |err| switch (err) { | |
| 1184 | error.FileNotFound => { | |
| 1185 | try self.makePath(sub_path); | |
| 1186 | return self.openDir(sub_path, open_dir_options); | |
| 1187 | }, | |
| 1188 | else => |e| return e, | |
| 1189 | }; | |
| 1190 | }, | |
| 1191 | }; | |
| 1192 | } | |
| 1193 | ||
| 1194 | /// This function returns the canonicalized absolute pathname of | |
| 1195 | /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this | |
| 1196 | /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` | |
| 1197 | /// argument. | |
| 1198 | /// This function is not universally supported by all platforms. | |
| 1199 | /// Currently supported hosts are: Linux, macOS, and Windows. | |
| 1200 | /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. | |
| 1201 | pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { | |
| 1202 | if (builtin.os.tag == .wasi) { | |
| 1203 | @compileError("realpath is not available on WASI"); | |
| 1204 | } | |
| 1205 | if (builtin.os.tag == .windows) { | |
| 1206 | const pathname_w = try std.os.windows.sliceToPrefixedFileW(self.fd, pathname); | |
| 1207 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1208 | } | |
| 1209 | const pathname_c = try posix.toPosixPath(pathname); | |
| 1210 | return self.realpathZ(&pathname_c, out_buffer); | |
| 1211 | } | |
| 1212 | ||
| 1213 | /// Same as `Dir.realpath` except `pathname` is null-terminated. | |
| 1214 | /// See also `Dir.realpath`, `realpathZ`. | |
| 1215 | pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { | |
| 1216 | if (builtin.os.tag == .windows) { | |
| 1217 | const pathname_w = try posix.windows.cStrToPrefixedFileW(self.fd, pathname); | |
| 1218 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 1219 | } | |
| 1220 | ||
| 1221 | const flags = if (builtin.os.tag == .linux) | |
| 1222 | posix.O.PATH | posix.O.NONBLOCK | posix.O.CLOEXEC | |
| 1223 | else | |
| 1224 | posix.O.NONBLOCK | posix.O.CLOEXEC; | |
| 1225 | const fd = posix.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { | |
| 1226 | error.FileLocksNotSupported => unreachable, | |
| 1227 | else => |e| return e, | |
| 1228 | }; | |
| 1229 | defer posix.close(fd); | |
| 1230 | ||
| 1231 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1232 | // have a variant that takes an arbitrary-size buffer. | |
| 1233 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1234 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1235 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1236 | // anyway. | |
| 1237 | var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1238 | const out_path = try posix.getFdPath(fd, &buffer); | |
| 1239 | ||
| 1240 | if (out_path.len > out_buffer.len) { | |
| 1241 | return error.NameTooLong; | |
| 1242 | } | |
| 1243 | ||
| 1244 | const result = out_buffer[0..out_path.len]; | |
| 1245 | @memcpy(result, out_path); | |
| 1246 | return result; | |
| 1247 | } | |
| 1248 | ||
| 1249 | /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. | |
| 1250 | /// See also `Dir.realpath`, `realpathW`. | |
| 1251 | pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { | |
| 1252 | const w = std.os.windows; | |
| 1253 | ||
| 1254 | const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; | |
| 1255 | const share_access = w.FILE_SHARE_READ; | |
| 1256 | const creation = w.FILE_OPEN; | |
| 1257 | const h_file = blk: { | |
| 1258 | const res = w.OpenFile(pathname, .{ | |
| 1259 | .dir = self.fd, | |
| 1260 | .access_mask = access_mask, | |
| 1261 | .share_access = share_access, | |
| 1262 | .creation = creation, | |
| 1263 | .io_mode = .blocking, | |
| 1264 | .filter = .any, | |
| 1265 | }) catch |err| switch (err) { | |
| 1266 | error.WouldBlock => unreachable, | |
| 1267 | else => |e| return e, | |
| 1268 | }; | |
| 1269 | break :blk res; | |
| 1270 | }; | |
| 1271 | defer w.CloseHandle(h_file); | |
| 1272 | ||
| 1273 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1274 | // have a variant that takes an arbitrary-size buffer. | |
| 1275 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1276 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1277 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1278 | // anyway. | |
| 1279 | var buffer: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1280 | const out_path = try posix.getFdPath(h_file, &buffer); | |
| 1281 | ||
| 1282 | if (out_path.len > out_buffer.len) { | |
| 1283 | return error.NameTooLong; | |
| 1284 | } | |
| 1285 | ||
| 1286 | const result = out_buffer[0..out_path.len]; | |
| 1287 | @memcpy(result, out_path); | |
| 1288 | return result; | |
| 1289 | } | |
| 1290 | ||
| 1291 | /// Same as `Dir.realpath` except caller must free the returned memory. | |
| 1292 | /// See also `Dir.realpath`. | |
| 1293 | pub fn realpathAlloc(self: Dir, allocator: Allocator, pathname: []const u8) ![]u8 { | |
| 1294 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1295 | // have a variant that takes an arbitrary-size buffer. | |
| 1296 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1297 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1298 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1299 | // anyway. | |
| 1300 | var buf: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 1301 | return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); | |
| 1302 | } | |
| 1303 | ||
| 1304 | /// Changes the current working directory to the open directory handle. | |
| 1305 | /// This modifies global state and can have surprising effects in multi- | |
| 1306 | /// threaded applications. Most applications and especially libraries should | |
| 1307 | /// not call this function as a general rule, however it can have use cases | |
| 1308 | /// in, for example, implementing a shell, or child process execution. | |
| 1309 | /// Not all targets support this. For example, WASI does not have the concept | |
| 1310 | /// of a current working directory. | |
| 1311 | pub fn setAsCwd(self: Dir) !void { | |
| 1312 | if (builtin.os.tag == .wasi) { | |
| 1313 | @compileError("changing cwd is not currently possible in WASI"); | |
| 1314 | } | |
| 1315 | if (builtin.os.tag == .windows) { | |
| 1316 | var dir_path_buffer: [std.os.windows.PATH_MAX_WIDE]u16 = undefined; | |
| 1317 | const dir_path = try std.os.windows.GetFinalPathNameByHandle(self.fd, .{}, &dir_path_buffer); | |
| 1318 | if (builtin.link_libc) { | |
| 1319 | return posix.chdirW(dir_path); | |
| 1320 | } | |
| 1321 | return std.os.windows.SetCurrentDirectory(dir_path); | |
| 1322 | } | |
| 1323 | try posix.fchdir(self.fd); | |
| 1324 | } | |
| 1325 | ||
| 1326 | pub const OpenDirOptions = struct { | |
| 1327 | /// `true` means the opened directory can be used as the `Dir` parameter | |
| 1328 | /// for functions which operate based on an open directory handle. When `false`, | |
| 1329 | /// such operations are Illegal Behavior. | |
| 1330 | access_sub_paths: bool = true, | |
| 1331 | ||
| 1332 | /// `true` means the opened directory can be scanned for the files and sub-directories | |
| 1333 | /// of the result. It means the `iterate` function can be called. | |
| 1334 | iterate: bool = false, | |
| 1335 | ||
| 1336 | /// `true` means it won't dereference the symlinks. | |
| 1337 | no_follow: bool = false, | |
| 1338 | }; | |
| 1339 | ||
| 1340 | /// Opens a directory at the given path. The directory is a system resource that remains | |
| 1341 | /// open until `close` is called on the result. | |
| 1342 | /// The directory cannot be iterated unless the `iterate` option is set to `true`. | |
| 1343 | /// | |
| 1344 | /// Asserts that the path parameter has no null bytes. | |
| 1345 | pub fn openDir(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1346 | if (builtin.os.tag == .windows) { | |
| 1347 | const sub_path_w = try posix.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1348 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1349 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1350 | return self.openDirWasi(sub_path, args); | |
| 1351 | } else { | |
| 1352 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1353 | return self.openDirZ(&sub_path_c, args); | |
| 1354 | } | |
| 1355 | } | |
| 1356 | ||
| 1357 | /// Same as `openDir` except only WASI. | |
| 1358 | pub fn openDirWasi(self: Dir, sub_path: []const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1359 | const w = std.os.wasi; | |
| 1360 | var base: w.rights_t = w.RIGHT.FD_FILESTAT_GET | w.RIGHT.FD_FDSTAT_SET_FLAGS | w.RIGHT.FD_FILESTAT_SET_TIMES; | |
| 1361 | if (args.access_sub_paths) { | |
| 1362 | base |= w.RIGHT.FD_READDIR | | |
| 1363 | w.RIGHT.PATH_CREATE_DIRECTORY | | |
| 1364 | w.RIGHT.PATH_CREATE_FILE | | |
| 1365 | w.RIGHT.PATH_LINK_SOURCE | | |
| 1366 | w.RIGHT.PATH_LINK_TARGET | | |
| 1367 | w.RIGHT.PATH_OPEN | | |
| 1368 | w.RIGHT.PATH_READLINK | | |
| 1369 | w.RIGHT.PATH_RENAME_SOURCE | | |
| 1370 | w.RIGHT.PATH_RENAME_TARGET | | |
| 1371 | w.RIGHT.PATH_FILESTAT_GET | | |
| 1372 | w.RIGHT.PATH_FILESTAT_SET_SIZE | | |
| 1373 | w.RIGHT.PATH_FILESTAT_SET_TIMES | | |
| 1374 | w.RIGHT.PATH_SYMLINK | | |
| 1375 | w.RIGHT.PATH_REMOVE_DIRECTORY | | |
| 1376 | w.RIGHT.PATH_UNLINK_FILE; | |
| 1377 | } | |
| 1378 | const symlink_flags: w.lookupflags_t = if (args.no_follow) 0x0 else w.LOOKUP_SYMLINK_FOLLOW; | |
| 1379 | // TODO do we really need all the rights here? | |
| 1380 | const inheriting: w.rights_t = w.RIGHT.ALL ^ w.RIGHT.SOCK_SHUTDOWN; | |
| 1381 | ||
| 1382 | const result = posix.openatWasi( | |
| 1383 | self.fd, | |
| 1384 | sub_path, | |
| 1385 | symlink_flags, | |
| 1386 | w.O.DIRECTORY, | |
| 1387 | 0x0, | |
| 1388 | base, | |
| 1389 | inheriting, | |
| 1390 | ); | |
| 1391 | const fd = result catch |err| switch (err) { | |
| 1392 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1393 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1394 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1395 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1396 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1397 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1398 | error.FileBusy => unreachable, // can't happen for directories | |
| 1399 | else => |e| return e, | |
| 1400 | }; | |
| 1401 | return Dir{ .fd = fd }; | |
| 1402 | } | |
| 1403 | ||
| 1404 | /// Same as `openDir` except the parameter is null-terminated. | |
| 1405 | pub fn openDirZ(self: Dir, sub_path_c: [*:0]const u8, args: OpenDirOptions) OpenError!Dir { | |
| 1406 | if (builtin.os.tag == .windows) { | |
| 1407 | const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1408 | return self.openDirW(sub_path_w.span().ptr, args); | |
| 1409 | } | |
| 1410 | const symlink_flags: u32 = if (args.no_follow) posix.O.NOFOLLOW else 0x0; | |
| 1411 | if (!args.iterate) { | |
| 1412 | const O_PATH = if (@hasDecl(posix.O, "PATH")) posix.O.PATH else 0; | |
| 1413 | return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | O_PATH | symlink_flags); | |
| 1414 | } else { | |
| 1415 | return self.openDirFlagsZ(sub_path_c, posix.O.DIRECTORY | posix.O.RDONLY | posix.O.CLOEXEC | symlink_flags); | |
| 1416 | } | |
| 1417 | } | |
| 1418 | ||
| 1419 | /// Same as `openDir` except the path parameter is WTF-16 encoded, NT-prefixed. | |
| 1420 | /// This function asserts the target OS is Windows. | |
| 1421 | pub fn openDirW(self: Dir, sub_path_w: [*:0]const u16, args: OpenDirOptions) OpenError!Dir { | |
| 1422 | const w = std.os.windows; | |
| 1423 | // TODO remove some of these flags if args.access_sub_paths is false | |
| 1424 | const base_flags = w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | | |
| 1425 | w.SYNCHRONIZE | w.FILE_TRAVERSE; | |
| 1426 | const flags: u32 = if (args.iterate) base_flags | w.FILE_LIST_DIRECTORY else base_flags; | |
| 1427 | const dir = try self.makeOpenDirAccessMaskW(sub_path_w, flags, .{ | |
| 1428 | .no_follow = args.no_follow, | |
| 1429 | .create_disposition = w.FILE_OPEN, | |
| 1430 | }); | |
| 1431 | return dir; | |
| 1432 | } | |
| 1433 | ||
| 1434 | /// `flags` must contain `posix.O.DIRECTORY`. | |
| 1435 | fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: u32) OpenError!Dir { | |
| 1436 | const result = if (fs.need_async_thread) | |
| 1437 | std.event.Loop.instance.?.openatZ(self.fd, sub_path_c, flags, 0) | |
| 1438 | else | |
| 1439 | posix.openatZ(self.fd, sub_path_c, flags, 0); | |
| 1440 | const fd = result catch |err| switch (err) { | |
| 1441 | error.FileTooBig => unreachable, // can't happen for directories | |
| 1442 | error.IsDir => unreachable, // we're providing O.DIRECTORY | |
| 1443 | error.NoSpaceLeft => unreachable, // not providing O.CREAT | |
| 1444 | error.PathAlreadyExists => unreachable, // not providing O.CREAT | |
| 1445 | error.FileLocksNotSupported => unreachable, // locking folders is not supported | |
| 1446 | error.WouldBlock => unreachable, // can't happen for directories | |
| 1447 | error.FileBusy => unreachable, // can't happen for directories | |
| 1448 | else => |e| return e, | |
| 1449 | }; | |
| 1450 | return Dir{ .fd = fd }; | |
| 1451 | } | |
| 1452 | ||
| 1453 | const MakeOpenDirAccessMaskWOptions = struct { | |
| 1454 | no_follow: bool, | |
| 1455 | create_disposition: u32, | |
| 1456 | }; | |
| 1457 | ||
| 1458 | fn makeOpenDirAccessMaskW(self: Dir, sub_path_w: [*:0]const u16, access_mask: u32, flags: MakeOpenDirAccessMaskWOptions) OpenError!Dir { | |
| 1459 | const w = std.os.windows; | |
| 1460 | ||
| 1461 | var result = Dir{ | |
| 1462 | .fd = undefined, | |
| 1463 | }; | |
| 1464 | ||
| 1465 | const path_len_bytes = @as(u16, @intCast(mem.sliceTo(sub_path_w, 0).len * 2)); | |
| 1466 | var nt_name = w.UNICODE_STRING{ | |
| 1467 | .Length = path_len_bytes, | |
| 1468 | .MaximumLength = path_len_bytes, | |
| 1469 | .Buffer = @constCast(sub_path_w), | |
| 1470 | }; | |
| 1471 | var attr = w.OBJECT_ATTRIBUTES{ | |
| 1472 | .Length = @sizeOf(w.OBJECT_ATTRIBUTES), | |
| 1473 | .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd, | |
| 1474 | .Attributes = 0, // Note we do not use OBJ_CASE_INSENSITIVE here. | |
| 1475 | .ObjectName = &nt_name, | |
| 1476 | .SecurityDescriptor = null, | |
| 1477 | .SecurityQualityOfService = null, | |
| 1478 | }; | |
| 1479 | const open_reparse_point: w.DWORD = if (flags.no_follow) w.FILE_OPEN_REPARSE_POINT else 0x0; | |
| 1480 | var io: w.IO_STATUS_BLOCK = undefined; | |
| 1481 | const rc = w.ntdll.NtCreateFile( | |
| 1482 | &result.fd, | |
| 1483 | access_mask, | |
| 1484 | &attr, | |
| 1485 | &io, | |
| 1486 | null, | |
| 1487 | w.FILE_ATTRIBUTE_NORMAL, | |
| 1488 | w.FILE_SHARE_READ | w.FILE_SHARE_WRITE, | |
| 1489 | flags.create_disposition, | |
| 1490 | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point, | |
| 1491 | null, | |
| 1492 | 0, | |
| 1493 | ); | |
| 1494 | ||
| 1495 | switch (rc) { | |
| 1496 | .SUCCESS => return result, | |
| 1497 | .OBJECT_NAME_INVALID => return error.BadPathName, | |
| 1498 | .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, | |
| 1499 | .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, | |
| 1500 | .NOT_A_DIRECTORY => return error.NotDir, | |
| 1501 | // This can happen if the directory has 'List folder contents' permission set to 'Deny' | |
| 1502 | // and the directory is trying to be opened for iteration. | |
| 1503 | .ACCESS_DENIED => return error.AccessDenied, | |
| 1504 | .INVALID_PARAMETER => unreachable, | |
| 1505 | else => return w.unexpectedStatus(rc), | |
| 1506 | } | |
| 1507 | } | |
| 1508 | ||
| 1509 | pub const DeleteFileError = posix.UnlinkError; | |
| 1510 | ||
| 1511 | /// Delete a file name and possibly the file it refers to, based on an open directory handle. | |
| 1512 | /// Asserts that the path parameter has no null bytes. | |
| 1513 | pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { | |
| 1514 | if (builtin.os.tag == .windows) { | |
| 1515 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1516 | return self.deleteFileW(sub_path_w.span()); | |
| 1517 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1518 | posix.unlinkat(self.fd, sub_path, 0) catch |err| switch (err) { | |
| 1519 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1520 | else => |e| return e, | |
| 1521 | }; | |
| 1522 | } else { | |
| 1523 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1524 | return self.deleteFileZ(&sub_path_c); | |
| 1525 | } | |
| 1526 | } | |
| 1527 | ||
| 1528 | /// Same as `deleteFile` except the parameter is null-terminated. | |
| 1529 | pub fn deleteFileZ(self: Dir, sub_path_c: [*:0]const u8) DeleteFileError!void { | |
| 1530 | posix.unlinkatZ(self.fd, sub_path_c, 0) catch |err| switch (err) { | |
| 1531 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1532 | error.AccessDenied => |e| switch (builtin.os.tag) { | |
| 1533 | // non-Linux POSIX systems return EPERM when trying to delete a directory, so | |
| 1534 | // we need to handle that case specifically and translate the error | |
| 1535 | .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris, .illumos => { | |
| 1536 | // Don't follow symlinks to match unlinkat (which acts on symlinks rather than follows them) | |
| 1537 | const fstat = posix.fstatatZ(self.fd, sub_path_c, posix.AT.SYMLINK_NOFOLLOW) catch return e; | |
| 1538 | const is_dir = fstat.mode & posix.S.IFMT == posix.S.IFDIR; | |
| 1539 | return if (is_dir) error.IsDir else e; | |
| 1540 | }, | |
| 1541 | else => return e, | |
| 1542 | }, | |
| 1543 | else => |e| return e, | |
| 1544 | }; | |
| 1545 | } | |
| 1546 | ||
| 1547 | /// Same as `deleteFile` except the parameter is WTF-16 encoded. | |
| 1548 | pub fn deleteFileW(self: Dir, sub_path_w: []const u16) DeleteFileError!void { | |
| 1549 | posix.unlinkatW(self.fd, sub_path_w, 0) catch |err| switch (err) { | |
| 1550 | error.DirNotEmpty => unreachable, // not passing AT.REMOVEDIR | |
| 1551 | else => |e| return e, | |
| 1552 | }; | |
| 1553 | } | |
| 1554 | ||
| 1555 | pub const DeleteDirError = error{ | |
| 1556 | DirNotEmpty, | |
| 1557 | FileNotFound, | |
| 1558 | AccessDenied, | |
| 1559 | FileBusy, | |
| 1560 | FileSystem, | |
| 1561 | SymLinkLoop, | |
| 1562 | NameTooLong, | |
| 1563 | NotDir, | |
| 1564 | SystemResources, | |
| 1565 | ReadOnlyFileSystem, | |
| 1566 | InvalidUtf8, | |
| 1567 | BadPathName, | |
| 1568 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1569 | NetworkNotFound, | |
| 1570 | Unexpected, | |
| 1571 | }; | |
| 1572 | ||
| 1573 | /// Returns `error.DirNotEmpty` if the directory is not empty. | |
| 1574 | /// To delete a directory recursively, see `deleteTree`. | |
| 1575 | /// Asserts that the path parameter has no null bytes. | |
| 1576 | pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { | |
| 1577 | if (builtin.os.tag == .windows) { | |
| 1578 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1579 | return self.deleteDirW(sub_path_w.span()); | |
| 1580 | } else if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1581 | posix.unlinkat(self.fd, sub_path, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1582 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1583 | else => |e| return e, | |
| 1584 | }; | |
| 1585 | } else { | |
| 1586 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1587 | return self.deleteDirZ(&sub_path_c); | |
| 1588 | } | |
| 1589 | } | |
| 1590 | ||
| 1591 | /// Same as `deleteDir` except the parameter is null-terminated. | |
| 1592 | pub fn deleteDirZ(self: Dir, sub_path_c: [*:0]const u8) DeleteDirError!void { | |
| 1593 | posix.unlinkatZ(self.fd, sub_path_c, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1594 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1595 | else => |e| return e, | |
| 1596 | }; | |
| 1597 | } | |
| 1598 | ||
| 1599 | /// Same as `deleteDir` except the parameter is UTF16LE, NT prefixed. | |
| 1600 | /// This function is Windows-only. | |
| 1601 | pub fn deleteDirW(self: Dir, sub_path_w: []const u16) DeleteDirError!void { | |
| 1602 | posix.unlinkatW(self.fd, sub_path_w, posix.AT.REMOVEDIR) catch |err| switch (err) { | |
| 1603 | error.IsDir => unreachable, // not possible since we pass AT.REMOVEDIR | |
| 1604 | else => |e| return e, | |
| 1605 | }; | |
| 1606 | } | |
| 1607 | ||
| 1608 | pub const RenameError = posix.RenameError; | |
| 1609 | ||
| 1610 | /// Change the name or location of a file or directory. | |
| 1611 | /// If new_sub_path already exists, it will be replaced. | |
| 1612 | /// Renaming a file over an existing directory or a directory | |
| 1613 | /// over an existing file will fail with `error.IsDir` or `error.NotDir` | |
| 1614 | pub fn rename(self: Dir, old_sub_path: []const u8, new_sub_path: []const u8) RenameError!void { | |
| 1615 | return posix.renameat(self.fd, old_sub_path, self.fd, new_sub_path); | |
| 1616 | } | |
| 1617 | ||
| 1618 | /// Same as `rename` except the parameters are null-terminated. | |
| 1619 | pub fn renameZ(self: Dir, old_sub_path_z: [*:0]const u8, new_sub_path_z: [*:0]const u8) RenameError!void { | |
| 1620 | return posix.renameatZ(self.fd, old_sub_path_z, self.fd, new_sub_path_z); | |
| 1621 | } | |
| 1622 | ||
| 1623 | /// Same as `rename` except the parameters are UTF16LE, NT prefixed. | |
| 1624 | /// This function is Windows-only. | |
| 1625 | pub fn renameW(self: Dir, old_sub_path_w: []const u16, new_sub_path_w: []const u16) RenameError!void { | |
| 1626 | return posix.renameatW(self.fd, old_sub_path_w, self.fd, new_sub_path_w); | |
| 1627 | } | |
| 1628 | ||
| 1629 | /// Use with `Dir.symLink` and `symLinkAbsolute` to specify whether the symlink | |
| 1630 | /// will point to a file or a directory. This value is ignored on all hosts | |
| 1631 | /// except Windows where creating symlinks to different resource types, requires | |
| 1632 | /// different flags. By default, `symLinkAbsolute` is assumed to point to a file. | |
| 1633 | pub const SymLinkFlags = struct { | |
| 1634 | is_directory: bool = false, | |
| 1635 | }; | |
| 1636 | ||
| 1637 | /// Creates a symbolic link named `sym_link_path` which contains the string `target_path`. | |
| 1638 | /// A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent | |
| 1639 | /// one; the latter case is known as a dangling link. | |
| 1640 | /// If `sym_link_path` exists, it will not be overwritten. | |
| 1641 | pub fn symLink( | |
| 1642 | self: Dir, | |
| 1643 | target_path: []const u8, | |
| 1644 | sym_link_path: []const u8, | |
| 1645 | flags: SymLinkFlags, | |
| 1646 | ) !void { | |
| 1647 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1648 | return self.symLinkWasi(target_path, sym_link_path, flags); | |
| 1649 | } | |
| 1650 | if (builtin.os.tag == .windows) { | |
| 1651 | // Target path does not use sliceToPrefixedFileW because certain paths | |
| 1652 | // are handled differently when creating a symlink than they would be | |
| 1653 | // when converting to an NT namespaced path. CreateSymbolicLink in | |
| 1654 | // symLinkW will handle the necessary conversion. | |
| 1655 | var target_path_w: std.os.windows.PathSpace = undefined; | |
| 1656 | target_path_w.len = try std.unicode.utf8ToUtf16Le(&target_path_w.data, target_path); | |
| 1657 | target_path_w.data[target_path_w.len] = 0; | |
| 1658 | const sym_link_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sym_link_path); | |
| 1659 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1660 | } | |
| 1661 | const target_path_c = try posix.toPosixPath(target_path); | |
| 1662 | const sym_link_path_c = try posix.toPosixPath(sym_link_path); | |
| 1663 | return self.symLinkZ(&target_path_c, &sym_link_path_c, flags); | |
| 1664 | } | |
| 1665 | ||
| 1666 | /// WASI-only. Same as `symLink` except targeting WASI. | |
| 1667 | pub fn symLinkWasi( | |
| 1668 | self: Dir, | |
| 1669 | target_path: []const u8, | |
| 1670 | sym_link_path: []const u8, | |
| 1671 | _: SymLinkFlags, | |
| 1672 | ) !void { | |
| 1673 | return posix.symlinkat(target_path, self.fd, sym_link_path); | |
| 1674 | } | |
| 1675 | ||
| 1676 | /// Same as `symLink`, except the pathname parameters are null-terminated. | |
| 1677 | pub fn symLinkZ( | |
| 1678 | self: Dir, | |
| 1679 | target_path_c: [*:0]const u8, | |
| 1680 | sym_link_path_c: [*:0]const u8, | |
| 1681 | flags: SymLinkFlags, | |
| 1682 | ) !void { | |
| 1683 | if (builtin.os.tag == .windows) { | |
| 1684 | const target_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, target_path_c); | |
| 1685 | const sym_link_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sym_link_path_c); | |
| 1686 | return self.symLinkW(target_path_w.span(), sym_link_path_w.span(), flags); | |
| 1687 | } | |
| 1688 | return posix.symlinkatZ(target_path_c, self.fd, sym_link_path_c); | |
| 1689 | } | |
| 1690 | ||
| 1691 | /// Windows-only. Same as `symLink` except the pathname parameters | |
| 1692 | /// are null-terminated, WTF16 encoded. | |
| 1693 | pub fn symLinkW( | |
| 1694 | self: Dir, | |
| 1695 | /// WTF-16, does not need to be NT-prefixed. The NT-prefixing | |
| 1696 | /// of this path is handled by CreateSymbolicLink. | |
| 1697 | target_path_w: [:0]const u16, | |
| 1698 | /// WTF-16, must be NT-prefixed or relative | |
| 1699 | sym_link_path_w: []const u16, | |
| 1700 | flags: SymLinkFlags, | |
| 1701 | ) !void { | |
| 1702 | return std.os.windows.CreateSymbolicLink(self.fd, sym_link_path_w, target_path_w, flags.is_directory); | |
| 1703 | } | |
| 1704 | ||
| 1705 | pub const ReadLinkError = posix.ReadLinkError; | |
| 1706 | ||
| 1707 | /// Read value of a symbolic link. | |
| 1708 | /// The return value is a slice of `buffer`, from index `0`. | |
| 1709 | /// Asserts that the path parameter has no null bytes. | |
| 1710 | pub fn readLink(self: Dir, sub_path: []const u8, buffer: []u8) ReadLinkError![]u8 { | |
| 1711 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 1712 | return self.readLinkWasi(sub_path, buffer); | |
| 1713 | } | |
| 1714 | if (builtin.os.tag == .windows) { | |
| 1715 | const sub_path_w = try std.os.windows.sliceToPrefixedFileW(self.fd, sub_path); | |
| 1716 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 1717 | } | |
| 1718 | const sub_path_c = try posix.toPosixPath(sub_path); | |
| 1719 | return self.readLinkZ(&sub_path_c, buffer); | |
| 1720 | } | |
| 1721 | ||
| 1722 | /// WASI-only. Same as `readLink` except targeting WASI. | |
| 1723 | pub fn readLinkWasi(self: Dir, sub_path: []const u8, buffer: []u8) ![]u8 { | |
| 1724 | return posix.readlinkat(self.fd, sub_path, buffer); | |
| 1725 | } | |
| 1726 | ||
| 1727 | /// Same as `readLink`, except the `pathname` parameter is null-terminated. | |
| 1728 | pub fn readLinkZ(self: Dir, sub_path_c: [*:0]const u8, buffer: []u8) ![]u8 { | |
| 1729 | if (builtin.os.tag == .windows) { | |
| 1730 | const sub_path_w = try std.os.windows.cStrToPrefixedFileW(self.fd, sub_path_c); | |
| 1731 | return self.readLinkW(sub_path_w.span(), buffer); | |
| 1732 | } | |
| 1733 | return posix.readlinkatZ(self.fd, sub_path_c, buffer); | |
| 1734 | } | |
| 1735 | ||
| 1736 | /// Windows-only. Same as `readLink` except the pathname parameter | |
| 1737 | /// is null-terminated, WTF16 encoded. | |
| 1738 | pub fn readLinkW(self: Dir, sub_path_w: []const u16, buffer: []u8) ![]u8 { | |
| 1739 | return std.os.windows.ReadLink(self.fd, sub_path_w, buffer); | |
| 1740 | } | |
| 1741 | ||
| 1742 | /// Read all of file contents using a preallocated buffer. | |
| 1743 | /// The returned slice has the same pointer as `buffer`. If the length matches `buffer.len` | |
| 1744 | /// the situation is ambiguous. It could either mean that the entire file was read, and | |
| 1745 | /// it exactly fits the buffer, or it could mean the buffer was not big enough for the | |
| 1746 | /// entire file. | |
| 1747 | pub fn readFile(self: Dir, file_path: []const u8, buffer: []u8) ![]u8 { | |
| 1748 | var file = try self.openFile(file_path, .{}); | |
| 1749 | defer file.close(); | |
| 1750 | ||
| 1751 | const end_index = try file.readAll(buffer); | |
| 1752 | return buffer[0..end_index]; | |
| 1753 | } | |
| 1754 | ||
| 1755 | /// On success, caller owns returned buffer. | |
| 1756 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 1757 | pub fn readFileAlloc(self: Dir, allocator: mem.Allocator, file_path: []const u8, max_bytes: usize) ![]u8 { | |
| 1758 | return self.readFileAllocOptions(allocator, file_path, max_bytes, null, @alignOf(u8), null); | |
| 1759 | } | |
| 1760 | ||
| 1761 | /// On success, caller owns returned buffer. | |
| 1762 | /// If the file is larger than `max_bytes`, returns `error.FileTooBig`. | |
| 1763 | /// If `size_hint` is specified the initial buffer size is calculated using | |
| 1764 | /// that value, otherwise the effective file size is used instead. | |
| 1765 | /// Allows specifying alignment and a sentinel value. | |
| 1766 | pub fn readFileAllocOptions( | |
| 1767 | self: Dir, | |
| 1768 | allocator: mem.Allocator, | |
| 1769 | file_path: []const u8, | |
| 1770 | max_bytes: usize, | |
| 1771 | size_hint: ?usize, | |
| 1772 | comptime alignment: u29, | |
| 1773 | comptime optional_sentinel: ?u8, | |
| 1774 | ) !(if (optional_sentinel) |s| [:s]align(alignment) u8 else []align(alignment) u8) { | |
| 1775 | var file = try self.openFile(file_path, .{}); | |
| 1776 | defer file.close(); | |
| 1777 | ||
| 1778 | // If the file size doesn't fit a usize it'll be certainly greater than | |
| 1779 | // `max_bytes` | |
| 1780 | const stat_size = size_hint orelse std.math.cast(usize, try file.getEndPos()) orelse | |
| 1781 | return error.FileTooBig; | |
| 1782 | ||
| 1783 | return file.readToEndAllocOptions(allocator, max_bytes, stat_size, alignment, optional_sentinel); | |
| 1784 | } | |
| 1785 | ||
| 1786 | pub const DeleteTreeError = error{ | |
| 1787 | InvalidHandle, | |
| 1788 | AccessDenied, | |
| 1789 | FileTooBig, | |
| 1790 | SymLinkLoop, | |
| 1791 | ProcessFdQuotaExceeded, | |
| 1792 | NameTooLong, | |
| 1793 | SystemFdQuotaExceeded, | |
| 1794 | NoDevice, | |
| 1795 | SystemResources, | |
| 1796 | ReadOnlyFileSystem, | |
| 1797 | FileSystem, | |
| 1798 | FileBusy, | |
| 1799 | DeviceBusy, | |
| 1800 | ||
| 1801 | /// One of the path components was not a directory. | |
| 1802 | /// This error is unreachable if `sub_path` does not contain a path separator. | |
| 1803 | NotDir, | |
| 1804 | ||
| 1805 | /// On Windows, file paths must be valid Unicode. | |
| 1806 | InvalidUtf8, | |
| 1807 | ||
| 1808 | /// On Windows, file paths cannot contain these characters: | |
| 1809 | /// '/', '*', '?', '"', '<', '>', '|' | |
| 1810 | BadPathName, | |
| 1811 | ||
| 1812 | /// On Windows, `\\server` or `\\server\share` was not found. | |
| 1813 | NetworkNotFound, | |
| 1814 | } || posix.UnexpectedError; | |
| 1815 | ||
| 1816 | /// Whether `full_path` describes a symlink, file, or directory, this function | |
| 1817 | /// removes it. If it cannot be removed because it is a non-empty directory, | |
| 1818 | /// this function recursively removes its entries and then tries again. | |
| 1819 | /// This operation is not atomic on most file systems. | |
| 1820 | pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 1821 | var initial_iterable_dir = (try self.deleteTreeOpenInitialSubpath(sub_path, .file)) orelse return; | |
| 1822 | ||
| 1823 | const StackItem = struct { | |
| 1824 | name: []const u8, | |
| 1825 | parent_dir: Dir, | |
| 1826 | iter: Dir.Iterator, | |
| 1827 | ||
| 1828 | fn closeAll(items: []@This()) void { | |
| 1829 | for (items) |*item| item.iter.dir.close(); | |
| 1830 | } | |
| 1831 | }; | |
| 1832 | ||
| 1833 | var stack_buffer: [16]StackItem = undefined; | |
| 1834 | var stack = std.ArrayListUnmanaged(StackItem).initBuffer(&stack_buffer); | |
| 1835 | defer StackItem.closeAll(stack.items); | |
| 1836 | ||
| 1837 | stack.appendAssumeCapacity(.{ | |
| 1838 | .name = sub_path, | |
| 1839 | .parent_dir = self, | |
| 1840 | .iter = initial_iterable_dir.iterateAssumeFirstIteration(), | |
| 1841 | }); | |
| 1842 | ||
| 1843 | process_stack: while (stack.items.len != 0) { | |
| 1844 | var top = &stack.items[stack.items.len - 1]; | |
| 1845 | while (try top.iter.next()) |entry| { | |
| 1846 | var treat_as_dir = entry.kind == .directory; | |
| 1847 | handle_entry: while (true) { | |
| 1848 | if (treat_as_dir) { | |
| 1849 | if (stack.unusedCapacitySlice().len >= 1) { | |
| 1850 | var iterable_dir = top.iter.dir.openDir(entry.name, .{ | |
| 1851 | .no_follow = true, | |
| 1852 | .iterate = true, | |
| 1853 | }) catch |err| switch (err) { | |
| 1854 | error.NotDir => { | |
| 1855 | treat_as_dir = false; | |
| 1856 | continue :handle_entry; | |
| 1857 | }, | |
| 1858 | error.FileNotFound => { | |
| 1859 | // That's fine, we were trying to remove this directory anyway. | |
| 1860 | break :handle_entry; | |
| 1861 | }, | |
| 1862 | ||
| 1863 | error.InvalidHandle, | |
| 1864 | error.AccessDenied, | |
| 1865 | error.SymLinkLoop, | |
| 1866 | error.ProcessFdQuotaExceeded, | |
| 1867 | error.NameTooLong, | |
| 1868 | error.SystemFdQuotaExceeded, | |
| 1869 | error.NoDevice, | |
| 1870 | error.SystemResources, | |
| 1871 | error.Unexpected, | |
| 1872 | error.InvalidUtf8, | |
| 1873 | error.BadPathName, | |
| 1874 | error.NetworkNotFound, | |
| 1875 | error.DeviceBusy, | |
| 1876 | => |e| return e, | |
| 1877 | }; | |
| 1878 | stack.appendAssumeCapacity(.{ | |
| 1879 | .name = entry.name, | |
| 1880 | .parent_dir = top.iter.dir, | |
| 1881 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 1882 | }); | |
| 1883 | continue :process_stack; | |
| 1884 | } else { | |
| 1885 | try top.iter.dir.deleteTreeMinStackSizeWithKindHint(entry.name, entry.kind); | |
| 1886 | break :handle_entry; | |
| 1887 | } | |
| 1888 | } else { | |
| 1889 | if (top.iter.dir.deleteFile(entry.name)) { | |
| 1890 | break :handle_entry; | |
| 1891 | } else |err| switch (err) { | |
| 1892 | error.FileNotFound => break :handle_entry, | |
| 1893 | ||
| 1894 | // Impossible because we do not pass any path separators. | |
| 1895 | error.NotDir => unreachable, | |
| 1896 | ||
| 1897 | error.IsDir => { | |
| 1898 | treat_as_dir = true; | |
| 1899 | continue :handle_entry; | |
| 1900 | }, | |
| 1901 | ||
| 1902 | error.AccessDenied, | |
| 1903 | error.InvalidUtf8, | |
| 1904 | error.SymLinkLoop, | |
| 1905 | error.NameTooLong, | |
| 1906 | error.SystemResources, | |
| 1907 | error.ReadOnlyFileSystem, | |
| 1908 | error.FileSystem, | |
| 1909 | error.FileBusy, | |
| 1910 | error.BadPathName, | |
| 1911 | error.NetworkNotFound, | |
| 1912 | error.Unexpected, | |
| 1913 | => |e| return e, | |
| 1914 | } | |
| 1915 | } | |
| 1916 | } | |
| 1917 | } | |
| 1918 | ||
| 1919 | // On Windows, we can't delete until the dir's handle has been closed, so | |
| 1920 | // close it before we try to delete. | |
| 1921 | top.iter.dir.close(); | |
| 1922 | ||
| 1923 | // In order to avoid double-closing the directory when cleaning up | |
| 1924 | // the stack in the case of an error, we save the relevant portions and | |
| 1925 | // pop the value from the stack. | |
| 1926 | const parent_dir = top.parent_dir; | |
| 1927 | const name = top.name; | |
| 1928 | stack.items.len -= 1; | |
| 1929 | ||
| 1930 | var need_to_retry: bool = false; | |
| 1931 | parent_dir.deleteDir(name) catch |err| switch (err) { | |
| 1932 | error.FileNotFound => {}, | |
| 1933 | error.DirNotEmpty => need_to_retry = true, | |
| 1934 | else => |e| return e, | |
| 1935 | }; | |
| 1936 | ||
| 1937 | if (need_to_retry) { | |
| 1938 | // Since we closed the handle that the previous iterator used, we | |
| 1939 | // need to re-open the dir and re-create the iterator. | |
| 1940 | var iterable_dir = iterable_dir: { | |
| 1941 | var treat_as_dir = true; | |
| 1942 | handle_entry: while (true) { | |
| 1943 | if (treat_as_dir) { | |
| 1944 | break :iterable_dir parent_dir.openDir(name, .{ | |
| 1945 | .no_follow = true, | |
| 1946 | .iterate = true, | |
| 1947 | }) catch |err| switch (err) { | |
| 1948 | error.NotDir => { | |
| 1949 | treat_as_dir = false; | |
| 1950 | continue :handle_entry; | |
| 1951 | }, | |
| 1952 | error.FileNotFound => { | |
| 1953 | // That's fine, we were trying to remove this directory anyway. | |
| 1954 | continue :process_stack; | |
| 1955 | }, | |
| 1956 | ||
| 1957 | error.InvalidHandle, | |
| 1958 | error.AccessDenied, | |
| 1959 | error.SymLinkLoop, | |
| 1960 | error.ProcessFdQuotaExceeded, | |
| 1961 | error.NameTooLong, | |
| 1962 | error.SystemFdQuotaExceeded, | |
| 1963 | error.NoDevice, | |
| 1964 | error.SystemResources, | |
| 1965 | error.Unexpected, | |
| 1966 | error.InvalidUtf8, | |
| 1967 | error.BadPathName, | |
| 1968 | error.NetworkNotFound, | |
| 1969 | error.DeviceBusy, | |
| 1970 | => |e| return e, | |
| 1971 | }; | |
| 1972 | } else { | |
| 1973 | if (parent_dir.deleteFile(name)) { | |
| 1974 | continue :process_stack; | |
| 1975 | } else |err| switch (err) { | |
| 1976 | error.FileNotFound => continue :process_stack, | |
| 1977 | ||
| 1978 | // Impossible because we do not pass any path separators. | |
| 1979 | error.NotDir => unreachable, | |
| 1980 | ||
| 1981 | error.IsDir => { | |
| 1982 | treat_as_dir = true; | |
| 1983 | continue :handle_entry; | |
| 1984 | }, | |
| 1985 | ||
| 1986 | error.AccessDenied, | |
| 1987 | error.InvalidUtf8, | |
| 1988 | error.SymLinkLoop, | |
| 1989 | error.NameTooLong, | |
| 1990 | error.SystemResources, | |
| 1991 | error.ReadOnlyFileSystem, | |
| 1992 | error.FileSystem, | |
| 1993 | error.FileBusy, | |
| 1994 | error.BadPathName, | |
| 1995 | error.NetworkNotFound, | |
| 1996 | error.Unexpected, | |
| 1997 | => |e| return e, | |
| 1998 | } | |
| 1999 | } | |
| 2000 | } | |
| 2001 | }; | |
| 2002 | // We know there is room on the stack since we are just re-adding | |
| 2003 | // the StackItem that we previously popped. | |
| 2004 | stack.appendAssumeCapacity(.{ | |
| 2005 | .name = name, | |
| 2006 | .parent_dir = parent_dir, | |
| 2007 | .iter = iterable_dir.iterateAssumeFirstIteration(), | |
| 2008 | }); | |
| 2009 | continue :process_stack; | |
| 2010 | } | |
| 2011 | } | |
| 2012 | } | |
| 2013 | ||
| 2014 | /// Like `deleteTree`, but only keeps one `Iterator` active at a time to minimize the function's stack size. | |
| 2015 | /// This is slower than `deleteTree` but uses less stack space. | |
| 2016 | pub fn deleteTreeMinStackSize(self: Dir, sub_path: []const u8) DeleteTreeError!void { | |
| 2017 | return self.deleteTreeMinStackSizeWithKindHint(sub_path, .file); | |
| 2018 | } | |
| 2019 | ||
| 2020 | fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint: File.Kind) DeleteTreeError!void { | |
| 2021 | start_over: while (true) { | |
| 2022 | var dir = (try self.deleteTreeOpenInitialSubpath(sub_path, kind_hint)) orelse return; | |
| 2023 | var cleanup_dir_parent: ?Dir = null; | |
| 2024 | defer if (cleanup_dir_parent) |*d| d.close(); | |
| 2025 | ||
| 2026 | var cleanup_dir = true; | |
| 2027 | defer if (cleanup_dir) dir.close(); | |
| 2028 | ||
| 2029 | // Valid use of MAX_PATH_BYTES because dir_name_buf will only | |
| 2030 | // ever store a single path component that was returned from the | |
| 2031 | // filesystem. | |
| 2032 | var dir_name_buf: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 2033 | var dir_name: []const u8 = sub_path; | |
| 2034 | ||
| 2035 | // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. | |
| 2036 | // Go through each entry and if it is not a directory, delete it. If it is a directory, | |
| 2037 | // open it, and close the original directory. Repeat. Then start the entire operation over. | |
| 2038 | ||
| 2039 | scan_dir: while (true) { | |
| 2040 | var dir_it = dir.iterateAssumeFirstIteration(); | |
| 2041 | dir_it: while (try dir_it.next()) |entry| { | |
| 2042 | var treat_as_dir = entry.kind == .directory; | |
| 2043 | handle_entry: while (true) { | |
| 2044 | if (treat_as_dir) { | |
| 2045 | const new_dir = dir.openDir(entry.name, .{ | |
| 2046 | .no_follow = true, | |
| 2047 | .iterate = true, | |
| 2048 | }) catch |err| switch (err) { | |
| 2049 | error.NotDir => { | |
| 2050 | treat_as_dir = false; | |
| 2051 | continue :handle_entry; | |
| 2052 | }, | |
| 2053 | error.FileNotFound => { | |
| 2054 | // That's fine, we were trying to remove this directory anyway. | |
| 2055 | continue :dir_it; | |
| 2056 | }, | |
| 2057 | ||
| 2058 | error.InvalidHandle, | |
| 2059 | error.AccessDenied, | |
| 2060 | error.SymLinkLoop, | |
| 2061 | error.ProcessFdQuotaExceeded, | |
| 2062 | error.NameTooLong, | |
| 2063 | error.SystemFdQuotaExceeded, | |
| 2064 | error.NoDevice, | |
| 2065 | error.SystemResources, | |
| 2066 | error.Unexpected, | |
| 2067 | error.InvalidUtf8, | |
| 2068 | error.BadPathName, | |
| 2069 | error.NetworkNotFound, | |
| 2070 | error.DeviceBusy, | |
| 2071 | => |e| return e, | |
| 2072 | }; | |
| 2073 | if (cleanup_dir_parent) |*d| d.close(); | |
| 2074 | cleanup_dir_parent = dir; | |
| 2075 | dir = new_dir; | |
| 2076 | const result = dir_name_buf[0..entry.name.len]; | |
| 2077 | @memcpy(result, entry.name); | |
| 2078 | dir_name = result; | |
| 2079 | continue :scan_dir; | |
| 2080 | } else { | |
| 2081 | if (dir.deleteFile(entry.name)) { | |
| 2082 | continue :dir_it; | |
| 2083 | } else |err| switch (err) { | |
| 2084 | error.FileNotFound => continue :dir_it, | |
| 2085 | ||
| 2086 | // Impossible because we do not pass any path separators. | |
| 2087 | error.NotDir => unreachable, | |
| 2088 | ||
| 2089 | error.IsDir => { | |
| 2090 | treat_as_dir = true; | |
| 2091 | continue :handle_entry; | |
| 2092 | }, | |
| 2093 | ||
| 2094 | error.AccessDenied, | |
| 2095 | error.InvalidUtf8, | |
| 2096 | error.SymLinkLoop, | |
| 2097 | error.NameTooLong, | |
| 2098 | error.SystemResources, | |
| 2099 | error.ReadOnlyFileSystem, | |
| 2100 | error.FileSystem, | |
| 2101 | error.FileBusy, | |
| 2102 | error.BadPathName, | |
| 2103 | error.NetworkNotFound, | |
| 2104 | error.Unexpected, | |
| 2105 | => |e| return e, | |
| 2106 | } | |
| 2107 | } | |
| 2108 | } | |
| 2109 | } | |
| 2110 | // Reached the end of the directory entries, which means we successfully deleted all of them. | |
| 2111 | // Now to remove the directory itself. | |
| 2112 | dir.close(); | |
| 2113 | cleanup_dir = false; | |
| 2114 | ||
| 2115 | if (cleanup_dir_parent) |d| { | |
| 2116 | d.deleteDir(dir_name) catch |err| switch (err) { | |
| 2117 | // These two things can happen due to file system race conditions. | |
| 2118 | error.FileNotFound, error.DirNotEmpty => continue :start_over, | |
| 2119 | else => |e| return e, | |
| 2120 | }; | |
| 2121 | continue :start_over; | |
| 2122 | } else { | |
| 2123 | self.deleteDir(sub_path) catch |err| switch (err) { | |
| 2124 | error.FileNotFound => return, | |
| 2125 | error.DirNotEmpty => continue :start_over, | |
| 2126 | else => |e| return e, | |
| 2127 | }; | |
| 2128 | return; | |
| 2129 | } | |
| 2130 | } | |
| 2131 | } | |
| 2132 | } | |
| 2133 | ||
| 2134 | /// On successful delete, returns null. | |
| 2135 | fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File.Kind) !?Dir { | |
| 2136 | return iterable_dir: { | |
| 2137 | // Treat as a file by default | |
| 2138 | var treat_as_dir = kind_hint == .directory; | |
| 2139 | ||
| 2140 | handle_entry: while (true) { | |
| 2141 | if (treat_as_dir) { | |
| 2142 | break :iterable_dir self.openDir(sub_path, .{ | |
| 2143 | .no_follow = true, | |
| 2144 | .iterate = true, | |
| 2145 | }) catch |err| switch (err) { | |
| 2146 | error.NotDir => { | |
| 2147 | treat_as_dir = false; | |
| 2148 | continue :handle_entry; | |
| 2149 | }, | |
| 2150 | error.FileNotFound => { | |
| 2151 | // That's fine, we were trying to remove this directory anyway. | |
| 2152 | return null; | |
| 2153 | }, | |
| 2154 | ||
| 2155 | error.InvalidHandle, | |
| 2156 | error.AccessDenied, | |
| 2157 | error.SymLinkLoop, | |
| 2158 | error.ProcessFdQuotaExceeded, | |
| 2159 | error.NameTooLong, | |
| 2160 | error.SystemFdQuotaExceeded, | |
| 2161 | error.NoDevice, | |
| 2162 | error.SystemResources, | |
| 2163 | error.Unexpected, | |
| 2164 | error.InvalidUtf8, | |
| 2165 | error.BadPathName, | |
| 2166 | error.DeviceBusy, | |
| 2167 | error.NetworkNotFound, | |
| 2168 | => |e| return e, | |
| 2169 | }; | |
| 2170 | } else { | |
| 2171 | if (self.deleteFile(sub_path)) { | |
| 2172 | return null; | |
| 2173 | } else |err| switch (err) { | |
| 2174 | error.FileNotFound => return null, | |
| 2175 | ||
| 2176 | error.IsDir => { | |
| 2177 | treat_as_dir = true; | |
| 2178 | continue :handle_entry; | |
| 2179 | }, | |
| 2180 | ||
| 2181 | error.AccessDenied, | |
| 2182 | error.InvalidUtf8, | |
| 2183 | error.SymLinkLoop, | |
| 2184 | error.NameTooLong, | |
| 2185 | error.SystemResources, | |
| 2186 | error.ReadOnlyFileSystem, | |
| 2187 | error.NotDir, | |
| 2188 | error.FileSystem, | |
| 2189 | error.FileBusy, | |
| 2190 | error.BadPathName, | |
| 2191 | error.NetworkNotFound, | |
| 2192 | error.Unexpected, | |
| 2193 | => |e| return e, | |
| 2194 | } | |
| 2195 | } | |
| 2196 | } | |
| 2197 | }; | |
| 2198 | } | |
| 2199 | ||
| 2200 | pub const WriteFileError = File.WriteError || File.OpenError; | |
| 2201 | ||
| 2202 | /// Deprecated: use `writeFile2`. | |
| 2203 | pub fn writeFile(self: Dir, sub_path: []const u8, data: []const u8) WriteFileError!void { | |
| 2204 | return writeFile2(self, .{ | |
| 2205 | .sub_path = sub_path, | |
| 2206 | .data = data, | |
| 2207 | .flags = .{}, | |
| 2208 | }); | |
| 2209 | } | |
| 2210 | ||
| 2211 | pub const WriteFileOptions = struct { | |
| 2212 | sub_path: []const u8, | |
| 2213 | data: []const u8, | |
| 2214 | flags: File.CreateFlags = .{}, | |
| 2215 | }; | |
| 2216 | ||
| 2217 | /// Writes content to the file system, using the file creation flags provided. | |
| 2218 | pub fn writeFile2(self: Dir, options: WriteFileOptions) WriteFileError!void { | |
| 2219 | var file = try self.createFile(options.sub_path, options.flags); | |
| 2220 | defer file.close(); | |
| 2221 | try file.writeAll(options.data); | |
| 2222 | } | |
| 2223 | ||
| 2224 | pub const AccessError = posix.AccessError; | |
| 2225 | ||
| 2226 | /// Test accessing `path`. | |
| 2227 | /// `path` is UTF-8-encoded. | |
| 2228 | /// Be careful of Time-Of-Check-Time-Of-Use race conditions when using this function. | |
| 2229 | /// For example, instead of testing if a file exists and then opening it, just | |
| 2230 | /// open it and handle the error for file not found. | |
| 2231 | pub fn access(self: Dir, sub_path: []const u8, flags: File.OpenFlags) AccessError!void { | |
| 2232 | if (builtin.os.tag == .windows) { | |
| 2233 | const sub_path_w = std.os.windows.sliceToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2234 | error.AccessDenied => return error.PermissionDenied, | |
| 2235 | else => |e| return e, | |
| 2236 | }; | |
| 2237 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2238 | } | |
| 2239 | const path_c = try posix.toPosixPath(sub_path); | |
| 2240 | return self.accessZ(&path_c, flags); | |
| 2241 | } | |
| 2242 | ||
| 2243 | /// Same as `access` except the path parameter is null-terminated. | |
| 2244 | pub fn accessZ(self: Dir, sub_path: [*:0]const u8, flags: File.OpenFlags) AccessError!void { | |
| 2245 | if (builtin.os.tag == .windows) { | |
| 2246 | const sub_path_w = std.os.windows.cStrToPrefixedFileW(self.fd, sub_path) catch |err| switch (err) { | |
| 2247 | error.AccessDenied => return error.PermissionDenied, | |
| 2248 | else => |e| return e, | |
| 2249 | }; | |
| 2250 | return self.accessW(sub_path_w.span().ptr, flags); | |
| 2251 | } | |
| 2252 | const os_mode = switch (flags.mode) { | |
| 2253 | .read_only => @as(u32, posix.F_OK), | |
| 2254 | .write_only => @as(u32, posix.W_OK), | |
| 2255 | .read_write => @as(u32, posix.R_OK | posix.W_OK), | |
| 2256 | }; | |
| 2257 | const result = if (fs.need_async_thread and flags.intended_io_mode != .blocking) | |
| 2258 | std.event.Loop.instance.?.faccessatZ(self.fd, sub_path, os_mode, 0) | |
| 2259 | else | |
| 2260 | posix.faccessatZ(self.fd, sub_path, os_mode, 0); | |
| 2261 | return result; | |
| 2262 | } | |
| 2263 | ||
| 2264 | /// Same as `access` except asserts the target OS is Windows and the path parameter is | |
| 2265 | /// * WTF-16 encoded | |
| 2266 | /// * null-terminated | |
| 2267 | /// * NtDll prefixed | |
| 2268 | /// TODO currently this ignores `flags`. | |
| 2269 | pub fn accessW(self: Dir, sub_path_w: [*:0]const u16, flags: File.OpenFlags) AccessError!void { | |
| 2270 | _ = flags; | |
| 2271 | return posix.faccessatW(self.fd, sub_path_w, 0, 0); | |
| 2272 | } | |
| 2273 | ||
| 2274 | pub const CopyFileOptions = struct { | |
| 2275 | /// When this is `null` the mode is copied from the source file. | |
| 2276 | override_mode: ?File.Mode = null, | |
| 2277 | }; | |
| 2278 | ||
| 2279 | pub const PrevStatus = enum { | |
| 2280 | stale, | |
| 2281 | fresh, | |
| 2282 | }; | |
| 2283 | ||
| 2284 | /// Check the file size, mtime, and mode of `source_path` and `dest_path`. If they are equal, does nothing. | |
| 2285 | /// Otherwise, atomically copies `source_path` to `dest_path`. The destination file gains the mtime, | |
| 2286 | /// atime, and mode of the source file so that the next call to `updateFile` will not need a copy. | |
| 2287 | /// Returns the previous status of the file before updating. | |
| 2288 | /// If any of the directories do not exist for dest_path, they are created. | |
| 2289 | pub fn updateFile( | |
| 2290 | source_dir: Dir, | |
| 2291 | source_path: []const u8, | |
| 2292 | dest_dir: Dir, | |
| 2293 | dest_path: []const u8, | |
| 2294 | options: CopyFileOptions, | |
| 2295 | ) !PrevStatus { | |
| 2296 | var src_file = try source_dir.openFile(source_path, .{}); | |
| 2297 | defer src_file.close(); | |
| 2298 | ||
| 2299 | const src_stat = try src_file.stat(); | |
| 2300 | const actual_mode = options.override_mode orelse src_stat.mode; | |
| 2301 | check_dest_stat: { | |
| 2302 | const dest_stat = blk: { | |
| 2303 | var dest_file = dest_dir.openFile(dest_path, .{}) catch |err| switch (err) { | |
| 2304 | error.FileNotFound => break :check_dest_stat, | |
| 2305 | else => |e| return e, | |
| 2306 | }; | |
| 2307 | defer dest_file.close(); | |
| 2308 | ||
| 2309 | break :blk try dest_file.stat(); | |
| 2310 | }; | |
| 2311 | ||
| 2312 | if (src_stat.size == dest_stat.size and | |
| 2313 | src_stat.mtime == dest_stat.mtime and | |
| 2314 | actual_mode == dest_stat.mode) | |
| 2315 | { | |
| 2316 | return PrevStatus.fresh; | |
| 2317 | } | |
| 2318 | } | |
| 2319 | ||
| 2320 | if (fs.path.dirname(dest_path)) |dirname| { | |
| 2321 | try dest_dir.makePath(dirname); | |
| 2322 | } | |
| 2323 | ||
| 2324 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode }); | |
| 2325 | defer atomic_file.deinit(); | |
| 2326 | ||
| 2327 | try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size }); | |
| 2328 | try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime); | |
| 2329 | try atomic_file.finish(); | |
| 2330 | return PrevStatus.stale; | |
| 2331 | } | |
| 2332 | ||
| 2333 | pub const CopyFileError = File.OpenError || File.StatError || | |
| 2334 | AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError; | |
| 2335 | ||
| 2336 | /// Guaranteed to be atomic. | |
| 2337 | /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available, | |
| 2338 | /// there is a possibility of power loss or application termination leaving temporary files present | |
| 2339 | /// in the same directory as dest_path. | |
| 2340 | pub fn copyFile( | |
| 2341 | source_dir: Dir, | |
| 2342 | source_path: []const u8, | |
| 2343 | dest_dir: Dir, | |
| 2344 | dest_path: []const u8, | |
| 2345 | options: CopyFileOptions, | |
| 2346 | ) CopyFileError!void { | |
| 2347 | var in_file = try source_dir.openFile(source_path, .{}); | |
| 2348 | defer in_file.close(); | |
| 2349 | ||
| 2350 | var size: ?u64 = null; | |
| 2351 | const mode = options.override_mode orelse blk: { | |
| 2352 | const st = try in_file.stat(); | |
| 2353 | size = st.size; | |
| 2354 | break :blk st.mode; | |
| 2355 | }; | |
| 2356 | ||
| 2357 | var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); | |
| 2358 | defer atomic_file.deinit(); | |
| 2359 | ||
| 2360 | try copy_file(in_file.handle, atomic_file.file.handle, size); | |
| 2361 | try atomic_file.finish(); | |
| 2362 | } | |
| 2363 | ||
| 2364 | const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.SendFileError; | |
| 2365 | ||
| 2366 | // Transfer all the data between two file descriptors in the most efficient way. | |
| 2367 | // The copy starts at offset 0, the initial offsets are preserved. | |
| 2368 | // No metadata is transferred over. | |
| 2369 | fn copy_file(fd_in: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void { | |
| 2370 | if (comptime builtin.target.isDarwin()) { | |
| 2371 | const rc = posix.system.fcopyfile(fd_in, fd_out, null, posix.system.COPYFILE_DATA); | |
| 2372 | switch (posix.errno(rc)) { | |
| 2373 | .SUCCESS => return, | |
| 2374 | .INVAL => unreachable, | |
| 2375 | .NOMEM => return error.SystemResources, | |
| 2376 | // The source file is not a directory, symbolic link, or regular file. | |
| 2377 | // Try with the fallback path before giving up. | |
| 2378 | .OPNOTSUPP => {}, | |
| 2379 | else => |err| return posix.unexpectedErrno(err), | |
| 2380 | } | |
| 2381 | } | |
| 2382 | ||
| 2383 | if (builtin.os.tag == .linux) { | |
| 2384 | // Try copy_file_range first as that works at the FS level and is the | |
| 2385 | // most efficient method (if available). | |
| 2386 | var offset: u64 = 0; | |
| 2387 | cfr_loop: while (true) { | |
| 2388 | // The kernel checks the u64 value `offset+count` for overflow, use | |
| 2389 | // a 32 bit value so that the syscall won't return EINVAL except for | |
| 2390 | // impossibly large files (> 2^64-1 - 2^32-1). | |
| 2391 | const amt = try posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0); | |
| 2392 | // Terminate as soon as we have copied size bytes or no bytes | |
| 2393 | if (maybe_size) |s| { | |
| 2394 | if (s == amt) break :cfr_loop; | |
| 2395 | } | |
| 2396 | if (amt == 0) break :cfr_loop; | |
| 2397 | offset += amt; | |
| 2398 | } | |
| 2399 | return; | |
| 2400 | } | |
| 2401 | ||
| 2402 | // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the | |
| 2403 | // fallback code will copy the contents chunk by chunk. | |
| 2404 | const empty_iovec = [0]posix.iovec_const{}; | |
| 2405 | var offset: u64 = 0; | |
| 2406 | sendfile_loop: while (true) { | |
| 2407 | const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); | |
| 2408 | // Terminate as soon as we have copied size bytes or no bytes | |
| 2409 | if (maybe_size) |s| { | |
| 2410 | if (s == amt) break :sendfile_loop; | |
| 2411 | } | |
| 2412 | if (amt == 0) break :sendfile_loop; | |
| 2413 | offset += amt; | |
| 2414 | } | |
| 2415 | } | |
| 2416 | ||
| 2417 | pub const AtomicFileOptions = struct { | |
| 2418 | mode: File.Mode = File.default_mode, | |
| 2419 | }; | |
| 2420 | ||
| 2421 | /// Directly access the `.file` field, and then call `AtomicFile.finish` | |
| 2422 | /// to atomically replace `dest_path` with contents. | |
| 2423 | /// Always call `AtomicFile.deinit` to clean up, regardless of whether `AtomicFile.finish` succeeded. | |
| 2424 | /// `dest_path` must remain valid until `AtomicFile.deinit` is called. | |
| 2425 | pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions) !AtomicFile { | |
| 2426 | if (fs.path.dirname(dest_path)) |dirname| { | |
| 2427 | const dir = try self.openDir(dirname, .{}); | |
| 2428 | return AtomicFile.init(fs.path.basename(dest_path), options.mode, dir, true); | |
| 2429 | } else { | |
| 2430 | return AtomicFile.init(dest_path, options.mode, self, false); | |
| 2431 | } | |
| 2432 | } | |
| 2433 | ||
| 2434 | pub const Stat = File.Stat; | |
| 2435 | pub const StatError = File.StatError; | |
| 2436 | ||
| 2437 | pub fn stat(self: Dir) StatError!Stat { | |
| 2438 | const file: File = .{ | |
| 2439 | .handle = self.fd, | |
| 2440 | .capable_io_mode = .blocking, | |
| 2441 | }; | |
| 2442 | return file.stat(); | |
| 2443 | } | |
| 2444 | ||
| 2445 | pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError; | |
| 2446 | ||
| 2447 | /// Returns metadata for a file inside the directory. | |
| 2448 | /// | |
| 2449 | /// On Windows, this requires three syscalls. On other operating systems, it | |
| 2450 | /// only takes one. | |
| 2451 | /// | |
| 2452 | /// Symlinks are followed. | |
| 2453 | /// | |
| 2454 | /// `sub_path` may be absolute, in which case `self` is ignored. | |
| 2455 | pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { | |
| 2456 | if (builtin.os.tag == .windows) { | |
| 2457 | var file = try self.openFile(sub_path, .{}); | |
| 2458 | defer file.close(); | |
| 2459 | return file.stat(); | |
| 2460 | } | |
| 2461 | if (builtin.os.tag == .wasi and !builtin.link_libc) { | |
| 2462 | const st = try posix.fstatatWasi(self.fd, sub_path, posix.wasi.LOOKUP_SYMLINK_FOLLOW); | |
| 2463 | return Stat.fromSystem(st); | |
| 2464 | } | |
| 2465 | const st = try posix.fstatat(self.fd, sub_path, 0); | |
| 2466 | return Stat.fromSystem(st); | |
| 2467 | } | |
| 2468 | ||
| 2469 | pub const ChmodError = File.ChmodError; | |
| 2470 | ||
| 2471 | /// Changes the mode of the directory. | |
| 2472 | /// The process must have the correct privileges in order to do this | |
| 2473 | /// successfully, or must have the effective user ID matching the owner | |
| 2474 | /// of the directory. Additionally, the directory must have been opened | |
| 2475 | /// with `OpenDirOptions{ .iterate = true }`. | |
| 2476 | pub fn chmod(self: Dir, new_mode: File.Mode) ChmodError!void { | |
| 2477 | const file: File = .{ | |
| 2478 | .handle = self.fd, | |
| 2479 | .capable_io_mode = .blocking, | |
| 2480 | }; | |
| 2481 | try file.chmod(new_mode); | |
| 2482 | } | |
| 2483 | ||
| 2484 | /// Changes the owner and group of the directory. | |
| 2485 | /// The process must have the correct privileges in order to do this | |
| 2486 | /// successfully. The group may be changed by the owner of the directory to | |
| 2487 | /// any group of which the owner is a member. Additionally, the directory | |
| 2488 | /// must have been opened with `OpenDirOptions{ .iterate = true }`. If the | |
| 2489 | /// owner or group is specified as `null`, the ID is not changed. | |
| 2490 | pub fn chown(self: Dir, owner: ?File.Uid, group: ?File.Gid) ChownError!void { | |
| 2491 | const file: File = .{ | |
| 2492 | .handle = self.fd, | |
| 2493 | .capable_io_mode = .blocking, | |
| 2494 | }; | |
| 2495 | try file.chown(owner, group); | |
| 2496 | } | |
| 2497 | ||
| 2498 | pub const ChownError = File.ChownError; | |
| 2499 | ||
| 2500 | const Permissions = File.Permissions; | |
| 2501 | pub const SetPermissionsError = File.SetPermissionsError; | |
| 2502 | ||
| 2503 | /// Sets permissions according to the provided `Permissions` struct. | |
| 2504 | /// This method is *NOT* available on WASI | |
| 2505 | pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void { | |
| 2506 | const file: File = .{ | |
| 2507 | .handle = self.fd, | |
| 2508 | .capable_io_mode = .blocking, | |
| 2509 | }; | |
| 2510 | try file.setPermissions(permissions); | |
| 2511 | } | |
| 2512 | ||
| 2513 | const Metadata = File.Metadata; | |
| 2514 | pub const MetadataError = File.MetadataError; | |
| 2515 | ||
| 2516 | /// Returns a `Metadata` struct, representing the permissions on the directory | |
| 2517 | pub fn metadata(self: Dir) MetadataError!Metadata { | |
| 2518 | const file: File = .{ | |
| 2519 | .handle = self.fd, | |
| 2520 | .capable_io_mode = .blocking, | |
| 2521 | }; | |
| 2522 | return try file.metadata(); | |
| 2523 | } | |
| 2524 | ||
| 2525 | const Dir = @This(); | |
| 2526 | const builtin = @import("builtin"); | |
| 2527 | const std = @import("../std.zig"); | |
| 2528 | const File = std.fs.File; | |
| 2529 | const AtomicFile = std.fs.AtomicFile; | |
| 2530 | const posix = std.os; | |
| 2531 | const mem = std.mem; | |
| 2532 | const fs = std.fs; | |
| 2533 | const Allocator = std.mem.Allocator; |
lib/std/fs/test.zig+2-2| ... | ... | @@ -1486,7 +1486,7 @@ test ". and .. in fs.Dir functions" { |
| 1486 | 1486 | |
| 1487 | 1487 | try ctx.dir.writeFile(update_path, "something"); |
| 1488 | 1488 | const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{}); |
| 1489 | try testing.expectEqual(fs.PrevStatus.stale, prev_status); | |
| 1489 | try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); | |
| 1490 | 1490 | |
| 1491 | 1491 | try ctx.dir.deleteDir(subdir_path); |
| 1492 | 1492 | } |
| ... | ... | @@ -1532,7 +1532,7 @@ test ". and .. in absolute functions" { |
| 1532 | 1532 | try update_file.writeAll("something"); |
| 1533 | 1533 | update_file.close(); |
| 1534 | 1534 | const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{}); |
| 1535 | try testing.expectEqual(fs.PrevStatus.stale, prev_status); | |
| 1535 | try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status); | |
| 1536 | 1536 | |
| 1537 | 1537 | try fs.deleteDirAbsolute(subdir_path); |
| 1538 | 1538 | } |