| ... | ... | @@ -37,8 +37,6 @@ pub const MAX_PATH_BYTES = switch (builtin.os) { |
| 37 | 37 | else => @compileError("Unsupported OS"), |
| 38 | 38 | }; |
| 39 | 39 | |
| 40 | | pub const MAX_BUF_BYTES: usize = 8192; |
| 41 | | |
| 42 | 40 | // here we replace the standard +/ with -_ so that it can be used in a file name |
| 43 | 41 | const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char); |
| 44 | 42 | |
| ... | ... | @@ -337,136 +335,35 @@ pub fn deleteDirW(dir_path: [*]const u16) !void { |
| 337 | 335 | return os.rmdirW(dir_path); |
| 338 | 336 | } |
| 339 | 337 | |
| 340 | | const DeleteTreeError = error{ |
| 341 | | OutOfMemory, |
| 342 | | AccessDenied, |
| 343 | | FileTooBig, |
| 344 | | IsDir, |
| 345 | | SymLinkLoop, |
| 346 | | ProcessFdQuotaExceeded, |
| 347 | | NameTooLong, |
| 348 | | SystemFdQuotaExceeded, |
| 349 | | NoDevice, |
| 350 | | SystemResources, |
| 351 | | NoSpaceLeft, |
| 352 | | PathAlreadyExists, |
| 353 | | ReadOnlyFileSystem, |
| 354 | | NotDir, |
| 355 | | FileNotFound, |
| 356 | | FileSystem, |
| 357 | | FileBusy, |
| 358 | | DirNotEmpty, |
| 359 | | DeviceBusy, |
| 360 | | |
| 361 | | /// On Windows, file paths must be valid Unicode. |
| 362 | | InvalidUtf8, |
| 363 | | |
| 364 | | /// On Windows, file paths cannot contain these characters: |
| 365 | | /// '/', '*', '?', '"', '<', '>', '|' |
| 366 | | BadPathName, |
| 367 | | |
| 368 | | Unexpected, |
| 369 | | }; |
| 370 | | |
| 371 | | /// Whether `full_path` describes a symlink, file, or directory, this function |
| 372 | | /// removes it. If it cannot be removed because it is a non-empty directory, |
| 373 | | /// this function recursively removes its entries and then tries again. |
| 374 | | /// TODO determine if we can remove the allocator requirement |
| 375 | | /// https://github.com/ziglang/zig/issues/2886 |
| 376 | | pub fn deleteTree(full_path: []const u8) DeleteTreeError!void { |
| 377 | | start_over: while (true) { |
| 378 | | var got_access_denied = false; |
| 379 | | // First, try deleting the item as a file. This way we don't follow sym links. |
| 380 | | if (deleteFile(full_path)) { |
| 381 | | return; |
| 382 | | } else |err| switch (err) { |
| 383 | | error.FileNotFound => return, |
| 384 | | error.IsDir => {}, |
| 385 | | error.AccessDenied => got_access_denied = true, |
| 386 | | |
| 387 | | error.InvalidUtf8, |
| 388 | | error.SymLinkLoop, |
| 389 | | error.NameTooLong, |
| 390 | | error.SystemResources, |
| 391 | | error.ReadOnlyFileSystem, |
| 392 | | error.NotDir, |
| 393 | | error.FileSystem, |
| 394 | | error.FileBusy, |
| 395 | | error.BadPathName, |
| 396 | | error.Unexpected, |
| 397 | | => return err, |
| 398 | | } |
| 399 | | { |
| 400 | | var dir = Dir.open(full_path) catch |err| switch (err) { |
| 401 | | error.NotDir => { |
| 402 | | if (got_access_denied) { |
| 403 | | return error.AccessDenied; |
| 404 | | } |
| 405 | | continue :start_over; |
| 406 | | }, |
| 407 | | |
| 408 | | error.OutOfMemory, |
| 409 | | error.AccessDenied, |
| 410 | | error.FileTooBig, |
| 411 | | error.IsDir, |
| 412 | | error.SymLinkLoop, |
| 413 | | error.ProcessFdQuotaExceeded, |
| 414 | | error.NameTooLong, |
| 415 | | error.SystemFdQuotaExceeded, |
| 416 | | error.NoDevice, |
| 417 | | error.FileNotFound, |
| 418 | | error.SystemResources, |
| 419 | | error.NoSpaceLeft, |
| 420 | | error.PathAlreadyExists, |
| 421 | | error.Unexpected, |
| 422 | | error.InvalidUtf8, |
| 423 | | error.BadPathName, |
| 424 | | error.DeviceBusy, |
| 425 | | => return err, |
| 426 | | }; |
| 427 | | defer dir.close(); |
| 428 | | |
| 429 | | while (try dir.next()) |entry| { |
| 430 | | var full_entry_buf: [MAX_BUF_BYTES]u8 = undefined; |
| 431 | | const full_entry_path = full_entry_buf[0..]; |
| 432 | | mem.copy(u8, full_entry_path, full_path); |
| 433 | | full_entry_path[full_path.len] = path.sep; |
| 434 | | mem.copy(u8, full_entry_path[full_path.len + 1 ..], entry.name); |
| 435 | | |
| 436 | | try deleteTree(full_entry_path[0..full_path.len + entry.name.len + 1]); |
| 437 | | } |
| 438 | | } |
| 439 | | return deleteDir(full_path); |
| 338 | /// Removes a symlink, file, or directory. |
| 339 | /// If `full_path` is relative, this is equivalent to `Dir.deleteTree` with the |
| 340 | /// current working directory as the open directory handle. |
| 341 | /// If `full_path` is absolute, this is equivalent to `Dir.deleteTree` with the |
| 342 | /// base directory. |
| 343 | pub fn deleteTree(full_path: []const u8) !void { |
| 344 | if (path.isAbsolute(full_path)) { |
| 345 | const dirname = path.dirname(full_path) orelse return error{ |
| 346 | /// Attempt to remove the root file system path. |
| 347 | /// This error is unreachable if `full_path` is relative. |
| 348 | CannotDeleteRootDirectory, |
| 349 | }.CannotDeleteRootDirectory; |
| 350 | |
| 351 | var dir = try Dir.open(dirname); |
| 352 | defer dir.close(); |
| 353 | |
| 354 | return dir.deleteTree(path.basename(full_path)); |
| 355 | } else { |
| 356 | return Dir.posix_cwd.deleteTree(full_path); |
| 440 | 357 | } |
| 441 | 358 | } |
| 442 | 359 | |
| 443 | | /// TODO: separate this API into the one that opens directory handles to then subsequently open |
| 444 | | /// files, and into the one that reads files from an open directory handle. |
| 445 | 360 | pub const Dir = struct { |
| 446 | | handle: Handle, |
| 361 | fd: os.fd_t, |
| 447 | 362 | |
| 448 | | pub const Handle = switch (builtin.os) { |
| 449 | | .macosx, .ios, .freebsd, .netbsd => struct { |
| 450 | | fd: i32, |
| 451 | | seek: i64, |
| 452 | | buf: [MAX_BUF_BYTES]u8, |
| 453 | | index: usize, |
| 454 | | end_index: usize, |
| 455 | | }, |
| 456 | | .linux => struct { |
| 457 | | fd: i32, |
| 458 | | buf: [MAX_BUF_BYTES]u8, |
| 459 | | index: usize, |
| 460 | | end_index: usize, |
| 461 | | }, |
| 462 | | .windows => struct { |
| 463 | | handle: os.windows.HANDLE, |
| 464 | | find_file_data: os.windows.WIN32_FIND_DATAW, |
| 465 | | first: bool, |
| 466 | | name_data: [256]u8, |
| 467 | | }, |
| 468 | | else => @compileError("unimplemented"), |
| 469 | | }; |
| 363 | /// An open handle to the current working directory. |
| 364 | /// Closing this directory is safety-checked illegal behavior. |
| 365 | /// Not available on Windows. |
| 366 | pub const posix_cwd = Dir{ .fd = os.AT_FDCWD }; |
| 470 | 367 | |
| 471 | 368 | pub const Entry = struct { |
| 472 | 369 | name: []const u8, |
| ... | ... | @@ -485,269 +382,504 @@ pub const Dir = struct { |
| 485 | 382 | }; |
| 486 | 383 | }; |
| 487 | 384 | |
| 385 | pub const Iterator = switch (builtin.os) { |
| 386 | .macosx, .ios, .freebsd, .netbsd => struct { |
| 387 | dir: Dir, |
| 388 | seek: i64, |
| 389 | buf: [buffer_len]u8, |
| 390 | index: usize, |
| 391 | end_index: usize, |
| 392 | |
| 393 | pub const buffer_len = 8192; |
| 394 | |
| 395 | const Self = @This(); |
| 396 | |
| 397 | /// Memory such as file names referenced in this returned entry becomes invalid |
| 398 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. |
| 399 | pub fn next(self: *Self) !?Entry { |
| 400 | switch (builtin.os) { |
| 401 | .macosx, .ios => return self.nextDarwin(), |
| 402 | .freebsd, .netbsd => return self.nextBsd(), |
| 403 | else => @compileError("unimplemented"), |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | fn nextDarwin(self: *Self) !?Entry { |
| 408 | start_over: while (true) { |
| 409 | if (self.index >= self.end_index) { |
| 410 | while (true) { |
| 411 | const rc = os.system.__getdirentries64( |
| 412 | self.dir.fd, |
| 413 | &self.buf, |
| 414 | self.buf.len, |
| 415 | &self.seek, |
| 416 | ); |
| 417 | if (rc == 0) return null; |
| 418 | if (rc < 0) { |
| 419 | switch (os.errno(rc)) { |
| 420 | os.EBADF => unreachable, |
| 421 | os.EFAULT => unreachable, |
| 422 | os.ENOTDIR => unreachable, |
| 423 | os.EINVAL => unreachable, |
| 424 | else => |err| return os.unexpectedErrno(err), |
| 425 | } |
| 426 | } |
| 427 | self.index = 0; |
| 428 | self.end_index = @intCast(usize, rc); |
| 429 | break; |
| 430 | } |
| 431 | } |
| 432 | const darwin_entry = @ptrCast(*align(1) os.dirent, &self.buf[self.index]); |
| 433 | const next_index = self.index + darwin_entry.d_reclen; |
| 434 | self.index = next_index; |
| 435 | |
| 436 | const name = @ptrCast([*]u8, &darwin_entry.d_name)[0..darwin_entry.d_namlen]; |
| 437 | |
| 438 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 439 | continue :start_over; |
| 440 | } |
| 441 | |
| 442 | const entry_kind = switch (darwin_entry.d_type) { |
| 443 | os.DT_BLK => Entry.Kind.BlockDevice, |
| 444 | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 445 | os.DT_DIR => Entry.Kind.Directory, |
| 446 | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 447 | os.DT_LNK => Entry.Kind.SymLink, |
| 448 | os.DT_REG => Entry.Kind.File, |
| 449 | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 450 | os.DT_WHT => Entry.Kind.Whiteout, |
| 451 | else => Entry.Kind.Unknown, |
| 452 | }; |
| 453 | return Entry{ |
| 454 | .name = name, |
| 455 | .kind = entry_kind, |
| 456 | }; |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | fn nextBsd(self: *Self) !?Entry { |
| 461 | start_over: while (true) { |
| 462 | if (self.index >= self.end_index) { |
| 463 | while (true) { |
| 464 | const rc = os.system.getdirentries( |
| 465 | self.dir.fd, |
| 466 | self.buf[0..].ptr, |
| 467 | self.buf.len, |
| 468 | &self.seek, |
| 469 | ); |
| 470 | switch (os.errno(rc)) { |
| 471 | 0 => {}, |
| 472 | os.EBADF => unreachable, |
| 473 | os.EFAULT => unreachable, |
| 474 | os.ENOTDIR => unreachable, |
| 475 | os.EINVAL => unreachable, |
| 476 | else => |err| return os.unexpectedErrno(err), |
| 477 | } |
| 478 | if (rc == 0) return null; |
| 479 | self.index = 0; |
| 480 | self.end_index = @intCast(usize, rc); |
| 481 | break; |
| 482 | } |
| 483 | } |
| 484 | const freebsd_entry = @ptrCast(*align(1) os.dirent, &self.buf[self.index]); |
| 485 | const next_index = self.index + freebsd_entry.d_reclen; |
| 486 | self.index = next_index; |
| 487 | |
| 488 | const name = @ptrCast([*]u8, &freebsd_entry.d_name)[0..freebsd_entry.d_namlen]; |
| 489 | |
| 490 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 491 | continue :start_over; |
| 492 | } |
| 493 | |
| 494 | const entry_kind = switch (freebsd_entry.d_type) { |
| 495 | os.DT_BLK => Entry.Kind.BlockDevice, |
| 496 | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 497 | os.DT_DIR => Entry.Kind.Directory, |
| 498 | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 499 | os.DT_LNK => Entry.Kind.SymLink, |
| 500 | os.DT_REG => Entry.Kind.File, |
| 501 | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 502 | os.DT_WHT => Entry.Kind.Whiteout, |
| 503 | else => Entry.Kind.Unknown, |
| 504 | }; |
| 505 | return Entry{ |
| 506 | .name = name, |
| 507 | .kind = entry_kind, |
| 508 | }; |
| 509 | } |
| 510 | } |
| 511 | }, |
| 512 | .linux => struct { |
| 513 | dir: Dir, |
| 514 | buf: [buffer_len]u8, |
| 515 | index: usize, |
| 516 | end_index: usize, |
| 517 | |
| 518 | pub const buffer_len = 8192; |
| 519 | |
| 520 | const Self = @This(); |
| 521 | |
| 522 | /// Memory such as file names referenced in this returned entry becomes invalid |
| 523 | /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. |
| 524 | pub fn next(self: *Self) !?Entry { |
| 525 | start_over: while (true) { |
| 526 | if (self.index >= self.end_index) { |
| 527 | while (true) { |
| 528 | const rc = os.linux.getdents64(self.dir.fd, &self.buf, self.buf.len); |
| 529 | switch (os.linux.getErrno(rc)) { |
| 530 | 0 => {}, |
| 531 | os.EBADF => unreachable, |
| 532 | os.EFAULT => unreachable, |
| 533 | os.ENOTDIR => unreachable, |
| 534 | os.EINVAL => unreachable, |
| 535 | else => |err| return os.unexpectedErrno(err), |
| 536 | } |
| 537 | if (rc == 0) return null; |
| 538 | self.index = 0; |
| 539 | self.end_index = rc; |
| 540 | break; |
| 541 | } |
| 542 | } |
| 543 | const linux_entry = @ptrCast(*align(1) os.dirent64, &self.buf[self.index]); |
| 544 | const next_index = self.index + linux_entry.d_reclen; |
| 545 | self.index = next_index; |
| 546 | |
| 547 | const name = mem.toSlice(u8, @ptrCast([*]u8, &linux_entry.d_name)); |
| 548 | |
| 549 | // skip . and .. entries |
| 550 | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 551 | continue :start_over; |
| 552 | } |
| 553 | |
| 554 | const entry_kind = switch (linux_entry.d_type) { |
| 555 | os.DT_BLK => Entry.Kind.BlockDevice, |
| 556 | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 557 | os.DT_DIR => Entry.Kind.Directory, |
| 558 | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 559 | os.DT_LNK => Entry.Kind.SymLink, |
| 560 | os.DT_REG => Entry.Kind.File, |
| 561 | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 562 | else => Entry.Kind.Unknown, |
| 563 | }; |
| 564 | return Entry{ |
| 565 | .name = name, |
| 566 | .kind = entry_kind, |
| 567 | }; |
| 568 | } |
| 569 | } |
| 570 | }, |
| 571 | .windows => struct { |
| 572 | dir: Dir, |
| 573 | find_file_data: os.windows.WIN32_FIND_DATAW, |
| 574 | first: bool, |
| 575 | name_data: [256]u8, |
| 576 | }, |
| 577 | else => @compileError("unimplemented"), |
| 578 | }; |
| 579 | |
| 488 | 580 | pub const OpenError = error{ |
| 489 | 581 | FileNotFound, |
| 490 | 582 | NotDir, |
| 491 | 583 | AccessDenied, |
| 492 | | FileTooBig, |
| 493 | | IsDir, |
| 494 | 584 | SymLinkLoop, |
| 495 | 585 | ProcessFdQuotaExceeded, |
| 496 | 586 | NameTooLong, |
| 497 | 587 | SystemFdQuotaExceeded, |
| 498 | 588 | NoDevice, |
| 499 | 589 | SystemResources, |
| 500 | | NoSpaceLeft, |
| 501 | | PathAlreadyExists, |
| 502 | | OutOfMemory, |
| 503 | 590 | InvalidUtf8, |
| 504 | 591 | BadPathName, |
| 505 | 592 | DeviceBusy, |
| 593 | } || os.UnexpectedError; |
| 506 | 594 | |
| 507 | | Unexpected, |
| 508 | | }; |
| 509 | | |
| 510 | | /// Call close when done. |
| 511 | | /// TODO remove the allocator requirement from this API |
| 512 | | /// https://github.com/ziglang/zig/issues/2885 |
| 595 | /// Call `close` to free the directory handle. |
| 513 | 596 | pub fn open(dir_path: []const u8) OpenError!Dir { |
| 514 | | return Dir{ |
| 515 | | .handle = switch (builtin.os) { |
| 516 | | .windows => blk: { |
| 517 | | var find_file_data: os.windows.WIN32_FIND_DATAW = undefined; |
| 518 | | const handle = try os.windows.FindFirstFile(dir_path, &find_file_data); |
| 519 | | break :blk Handle{ |
| 520 | | .handle = handle, |
| 521 | | .find_file_data = find_file_data, // TODO guaranteed copy elision |
| 522 | | .first = true, |
| 523 | | .name_data = undefined, |
| 524 | | }; |
| 525 | | }, |
| 526 | | .macosx, .ios, .freebsd, .netbsd => Handle{ |
| 527 | | .fd = try os.open(dir_path, os.O_RDONLY | os.O_NONBLOCK | os.O_DIRECTORY | os.O_CLOEXEC, 0), |
| 528 | | .seek = 0, |
| 529 | | .index = 0, |
| 530 | | .end_index = 0, |
| 531 | | .buf = [_]u8{}, |
| 532 | | }, |
| 533 | | .linux => Handle{ |
| 534 | | .fd = try os.open(dir_path, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC, 0), |
| 535 | | .index = 0, |
| 536 | | .end_index = 0, |
| 537 | | .buf = [_]u8{}, |
| 538 | | }, |
| 539 | | else => @compileError("unimplemented"), |
| 540 | | }, |
| 541 | | }; |
| 597 | return posix_cwd.openDir(dir_path); |
| 542 | 598 | } |
| 543 | 599 | |
| 544 | | pub fn close(self: *Dir) void { |
| 545 | | if (os.windows.is_the_target) { |
| 546 | | return os.windows.FindClose(self.handle.handle); |
| 547 | | } |
| 548 | | os.close(self.handle.fd); |
| 600 | /// Same as `open` except the parameter is null-terminated. |
| 601 | pub fn openC(dir_path_c: [*]const u8) OpenError!Dir { |
| 602 | return posix_cwd.openDirC(dir_path_c); |
| 549 | 603 | } |
| 550 | 604 | |
| 551 | | /// Memory such as file names referenced in this returned entry becomes invalid |
| 552 | | /// with subsequent calls to next, as well as when this `Dir` is deinitialized. |
| 553 | | pub fn next(self: *Dir) !?Entry { |
| 554 | | switch (builtin.os) { |
| 555 | | .linux => return self.nextLinux(), |
| 556 | | .macosx, .ios => return self.nextDarwin(), |
| 557 | | .windows => return self.nextWindows(), |
| 558 | | .freebsd => return self.nextBsd(), |
| 559 | | .netbsd => return self.nextBsd(), |
| 560 | | else => @compileError("unimplemented"), |
| 605 | pub fn close(self: *Dir) void { |
| 606 | if (os.windows.is_the_target) { |
| 607 | @panic("TODO"); |
| 561 | 608 | } |
| 609 | os.close(self.fd); |
| 610 | self.* = undefined; |
| 562 | 611 | } |
| 563 | 612 | |
| 564 | | pub fn openRead(self: Dir, file_path: []const u8) os.OpenError!File { |
| 565 | | const path_c = try os.toPosixPath(file_path); |
| 613 | /// Call `File.close` on the result when done. |
| 614 | pub fn openRead(self: Dir, sub_path: []const u8) File.OpenError!File { |
| 615 | const path_c = try os.toPosixPath(sub_path); |
| 566 | 616 | return self.openReadC(&path_c); |
| 567 | 617 | } |
| 568 | 618 | |
| 569 | | pub fn openReadC(self: Dir, file_path: [*]const u8) OpenError!File { |
| 619 | /// Call `File.close` on the result when done. |
| 620 | pub fn openReadC(self: Dir, sub_path: [*]const u8) File.OpenError!File { |
| 570 | 621 | const flags = os.O_LARGEFILE | os.O_RDONLY; |
| 571 | | const fd = try os.openatC(self.handle.fd, file_path, flags, 0); |
| 622 | const fd = try os.openatC(self.fd, sub_path, flags, 0); |
| 572 | 623 | return File.openHandle(fd); |
| 573 | 624 | } |
| 574 | 625 | |
| 575 | | fn nextDarwin(self: *Dir) !?Entry { |
| 576 | | start_over: while (true) { |
| 577 | | if (self.handle.index >= self.handle.end_index) { |
| 578 | | while (true) { |
| 579 | | const rc = os.system.__getdirentries64( |
| 580 | | self.handle.fd, |
| 581 | | self.handle.buf[0..].ptr, |
| 582 | | self.handle.buf.len, |
| 583 | | &self.handle.seek, |
| 584 | | ); |
| 585 | | if (rc == 0) return null; |
| 586 | | if (rc < 0) { |
| 587 | | switch (os.errno(rc)) { |
| 588 | | os.EBADF => unreachable, |
| 589 | | os.EFAULT => unreachable, |
| 590 | | os.ENOTDIR => unreachable, |
| 591 | | os.EINVAL => unreachable, |
| 592 | | else => |err| return os.unexpectedErrno(err), |
| 593 | | } |
| 594 | | } |
| 595 | | self.handle.index = 0; |
| 596 | | self.handle.end_index = @intCast(usize, rc); |
| 597 | | break; |
| 598 | | } |
| 599 | | } |
| 600 | | const darwin_entry = @ptrCast(*align(1) os.dirent, &self.handle.buf[self.handle.index]); |
| 601 | | const next_index = self.handle.index + darwin_entry.d_reclen; |
| 602 | | self.handle.index = next_index; |
| 626 | /// Call `close` on the result when done. |
| 627 | pub fn openDir(self: Dir, sub_path: []const u8) OpenError!Dir { |
| 628 | const sub_path_c = try os.toPosixPath(sub_path); |
| 629 | return self.openDirC(&sub_path_c); |
| 630 | } |
| 603 | 631 | |
| 604 | | const name = @ptrCast([*]u8, &darwin_entry.d_name)[0..darwin_entry.d_namlen]; |
| 632 | /// Call `close` on the result when done. |
| 633 | pub fn openDirC(self: Dir, sub_path: [*]const u8) OpenError!Dir { |
| 634 | const flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC; |
| 635 | const fd = os.openatC(self.fd, sub_path, flags, 0) catch |err| switch (err) { |
| 636 | error.FileTooBig => unreachable, // can't happen for directories |
| 637 | error.IsDir => unreachable, // we're providing O_DIRECTORY |
| 638 | error.NoSpaceLeft => unreachable, // not providing O_CREAT |
| 639 | error.PathAlreadyExists => unreachable, // not providing O_CREAT |
| 640 | else => |e| return e, |
| 641 | }; |
| 642 | return Dir{ .fd = fd }; |
| 643 | } |
| 605 | 644 | |
| 606 | | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 607 | | continue :start_over; |
| 608 | | } |
| 645 | pub const DeleteFileError = os.UnlinkError; |
| 609 | 646 | |
| 610 | | const entry_kind = switch (darwin_entry.d_type) { |
| 611 | | os.DT_BLK => Entry.Kind.BlockDevice, |
| 612 | | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 613 | | os.DT_DIR => Entry.Kind.Directory, |
| 614 | | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 615 | | os.DT_LNK => Entry.Kind.SymLink, |
| 616 | | os.DT_REG => Entry.Kind.File, |
| 617 | | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 618 | | os.DT_WHT => Entry.Kind.Whiteout, |
| 619 | | else => Entry.Kind.Unknown, |
| 620 | | }; |
| 621 | | return Entry{ |
| 622 | | .name = name, |
| 623 | | .kind = entry_kind, |
| 624 | | }; |
| 625 | | } |
| 647 | /// Delete a file name and possibly the file it refers to, based on an open directory handle. |
| 648 | pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void { |
| 649 | const sub_path_c = try os.toPosixPath(sub_path); |
| 650 | return self.deleteFileC(&sub_path_c); |
| 626 | 651 | } |
| 627 | 652 | |
| 628 | | fn nextWindows(self: *Dir) !?Entry { |
| 629 | | while (true) { |
| 630 | | if (self.handle.first) { |
| 631 | | self.handle.first = false; |
| 632 | | } else { |
| 633 | | if (!try os.windows.FindNextFile(self.handle.handle, &self.handle.find_file_data)) |
| 634 | | return null; |
| 635 | | } |
| 636 | | const name_utf16le = mem.toSlice(u16, self.handle.find_file_data.cFileName[0..].ptr); |
| 637 | | if (mem.eql(u16, name_utf16le, [_]u16{'.'}) or mem.eql(u16, name_utf16le, [_]u16{ '.', '.' })) |
| 638 | | continue; |
| 639 | | // Trust that Windows gives us valid UTF-16LE |
| 640 | | const name_utf8_len = std.unicode.utf16leToUtf8(self.handle.name_data[0..], name_utf16le) catch unreachable; |
| 641 | | const name_utf8 = self.handle.name_data[0..name_utf8_len]; |
| 642 | | const kind = blk: { |
| 643 | | const attrs = self.handle.find_file_data.dwFileAttributes; |
| 644 | | if (attrs & os.windows.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.Directory; |
| 645 | | if (attrs & os.windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.SymLink; |
| 646 | | break :blk Entry.Kind.File; |
| 647 | | }; |
| 648 | | return Entry{ |
| 649 | | .name = name_utf8, |
| 650 | | .kind = kind, |
| 651 | | }; |
| 652 | | } |
| 653 | /// Same as `deleteFile` except the parameter is null-terminated. |
| 654 | pub fn deleteFileC(self: Dir, sub_path_c: [*]const u8) DeleteFileError!void { |
| 655 | os.unlinkatC(self.fd, sub_path_c, 0) catch |err| switch (err) { |
| 656 | error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR |
| 657 | else => |e| return e, |
| 658 | }; |
| 653 | 659 | } |
| 654 | 660 | |
| 655 | | fn nextLinux(self: *Dir) !?Entry { |
| 656 | | start_over: while (true) { |
| 657 | | if (self.handle.index >= self.handle.end_index) { |
| 658 | | while (true) { |
| 659 | | const rc = os.linux.getdents64(self.handle.fd, self.handle.buf[0..].ptr, self.handle.buf.len); |
| 660 | | switch (os.linux.getErrno(rc)) { |
| 661 | | 0 => {}, |
| 662 | | os.EBADF => unreachable, |
| 663 | | os.EFAULT => unreachable, |
| 664 | | os.ENOTDIR => unreachable, |
| 665 | | os.EINVAL => unreachable, |
| 666 | | else => |err| return os.unexpectedErrno(err), |
| 667 | | } |
| 668 | | if (rc == 0) return null; |
| 669 | | self.handle.index = 0; |
| 670 | | self.handle.end_index = rc; |
| 671 | | break; |
| 672 | | } |
| 673 | | } |
| 674 | | const linux_entry = @ptrCast(*align(1) os.dirent64, &self.handle.buf[self.handle.index]); |
| 675 | | const next_index = self.handle.index + linux_entry.d_reclen; |
| 676 | | self.handle.index = next_index; |
| 661 | pub const DeleteDirError = error{ |
| 662 | DirNotEmpty, |
| 663 | FileNotFound, |
| 664 | AccessDenied, |
| 665 | FileBusy, |
| 666 | FileSystem, |
| 667 | SymLinkLoop, |
| 668 | NameTooLong, |
| 669 | NotDir, |
| 670 | SystemResources, |
| 671 | ReadOnlyFileSystem, |
| 672 | InvalidUtf8, |
| 673 | BadPathName, |
| 674 | Unexpected, |
| 675 | }; |
| 677 | 676 | |
| 678 | | const name = mem.toSlice(u8, @ptrCast([*]u8, &linux_entry.d_name)); |
| 677 | /// Returns `error.DirNotEmpty` if the directory is not empty. |
| 678 | /// To delete a directory recursively, see `deleteTree`. |
| 679 | pub fn deleteDir(self: Dir, sub_path: []const u8) DeleteDirError!void { |
| 680 | const sub_path_c = try os.toPosixPath(sub_path); |
| 681 | return self.deleteDirC(&sub_path_c); |
| 682 | } |
| 679 | 683 | |
| 680 | | // skip . and .. entries |
| 681 | | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 682 | | continue :start_over; |
| 683 | | } |
| 684 | /// Same as `deleteDir` except the parameter is null-terminated. |
| 685 | pub fn deleteDirC(self: Dir, sub_path_c: [*]const u8) DeleteDirError!void { |
| 686 | os.unlinkatC(self.fd, sub_path_c, os.AT_REMOVEDIR) catch |err| switch (err) { |
| 687 | error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR |
| 688 | else => |e| return e, |
| 689 | }; |
| 690 | } |
| 684 | 691 | |
| 685 | | const entry_kind = switch (linux_entry.d_type) { |
| 686 | | os.DT_BLK => Entry.Kind.BlockDevice, |
| 687 | | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 688 | | os.DT_DIR => Entry.Kind.Directory, |
| 689 | | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 690 | | os.DT_LNK => Entry.Kind.SymLink, |
| 691 | | os.DT_REG => Entry.Kind.File, |
| 692 | | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 693 | | else => Entry.Kind.Unknown, |
| 694 | | }; |
| 695 | | return Entry{ |
| 696 | | .name = name, |
| 697 | | .kind = entry_kind, |
| 698 | | }; |
| 692 | pub fn iterate(self: Dir) Iterator { |
| 693 | switch (builtin.os) { |
| 694 | .macosx, .ios, .freebsd, .netbsd => return Iterator{ |
| 695 | .dir = self, |
| 696 | .seek = 0, |
| 697 | .index = 0, |
| 698 | .end_index = 0, |
| 699 | .buf = undefined, |
| 700 | }, |
| 701 | .linux => return Iterator{ |
| 702 | .dir = self, |
| 703 | .index = 0, |
| 704 | .end_index = 0, |
| 705 | .buf = undefined, |
| 706 | }, |
| 707 | .windows => @panic("TODO"), |
| 708 | else => @compileError("unimplemented"), |
| 699 | 709 | } |
| 700 | 710 | } |
| 701 | 711 | |
| 702 | | fn nextBsd(self: *Dir) !?Entry { |
| 703 | | start_over: while (true) { |
| 704 | | if (self.handle.index >= self.handle.end_index) { |
| 705 | | while (true) { |
| 706 | | const rc = os.system.getdirentries( |
| 707 | | self.handle.fd, |
| 708 | | self.handle.buf[0..].ptr, |
| 709 | | self.handle.buf.len, |
| 710 | | &self.handle.seek, |
| 711 | | ); |
| 712 | | switch (os.errno(rc)) { |
| 713 | | 0 => {}, |
| 714 | | os.EBADF => unreachable, |
| 715 | | os.EFAULT => unreachable, |
| 716 | | os.ENOTDIR => unreachable, |
| 717 | | os.EINVAL => unreachable, |
| 718 | | else => |err| return os.unexpectedErrno(err), |
| 719 | | } |
| 720 | | if (rc == 0) return null; |
| 721 | | self.handle.index = 0; |
| 722 | | self.handle.end_index = @intCast(usize, rc); |
| 723 | | break; |
| 724 | | } |
| 725 | | } |
| 726 | | const freebsd_entry = @ptrCast(*align(1) os.dirent, &self.handle.buf[self.handle.index]); |
| 727 | | const next_index = self.handle.index + freebsd_entry.d_reclen; |
| 728 | | self.handle.index = next_index; |
| 712 | pub const DeleteTreeError = error{ |
| 713 | AccessDenied, |
| 714 | FileTooBig, |
| 715 | SymLinkLoop, |
| 716 | ProcessFdQuotaExceeded, |
| 717 | NameTooLong, |
| 718 | SystemFdQuotaExceeded, |
| 719 | NoDevice, |
| 720 | SystemResources, |
| 721 | ReadOnlyFileSystem, |
| 722 | FileSystem, |
| 723 | FileBusy, |
| 724 | DeviceBusy, |
| 729 | 725 | |
| 730 | | const name = @ptrCast([*]u8, &freebsd_entry.d_name)[0..freebsd_entry.d_namlen]; |
| 726 | /// One of the path components was not a directory. |
| 727 | /// This error is unreachable if `sub_path` does not contain a path separator. |
| 728 | NotDir, |
| 731 | 729 | |
| 732 | | if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) { |
| 733 | | continue :start_over; |
| 730 | /// On Windows, file paths must be valid Unicode. |
| 731 | InvalidUtf8, |
| 732 | |
| 733 | /// On Windows, file paths cannot contain these characters: |
| 734 | /// '/', '*', '?', '"', '<', '>', '|' |
| 735 | BadPathName, |
| 736 | } || os.UnexpectedError; |
| 737 | |
| 738 | /// Whether `full_path` describes a symlink, file, or directory, this function |
| 739 | /// removes it. If it cannot be removed because it is a non-empty directory, |
| 740 | /// this function recursively removes its entries and then tries again. |
| 741 | /// This operation is not atomic on most file systems. |
| 742 | pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { |
| 743 | start_over: while (true) { |
| 744 | var got_access_denied = false; |
| 745 | // First, try deleting the item as a file. This way we don't follow sym links. |
| 746 | if (self.deleteFile(sub_path)) { |
| 747 | return; |
| 748 | } else |err| switch (err) { |
| 749 | error.FileNotFound => return, |
| 750 | error.IsDir => {}, |
| 751 | error.AccessDenied => got_access_denied = true, |
| 752 | |
| 753 | error.InvalidUtf8, |
| 754 | error.SymLinkLoop, |
| 755 | error.NameTooLong, |
| 756 | error.SystemResources, |
| 757 | error.ReadOnlyFileSystem, |
| 758 | error.NotDir, |
| 759 | error.FileSystem, |
| 760 | error.FileBusy, |
| 761 | error.BadPathName, |
| 762 | error.Unexpected, |
| 763 | => |e| return e, |
| 734 | 764 | } |
| 765 | var dir = self.openDir(sub_path) catch |err| switch (err) { |
| 766 | error.NotDir => { |
| 767 | if (got_access_denied) { |
| 768 | return error.AccessDenied; |
| 769 | } |
| 770 | continue :start_over; |
| 771 | }, |
| 772 | error.FileNotFound => { |
| 773 | // That's fine, we were trying to remove this directory anyway. |
| 774 | continue :start_over; |
| 775 | }, |
| 735 | 776 | |
| 736 | | const entry_kind = switch (freebsd_entry.d_type) { |
| 737 | | os.DT_BLK => Entry.Kind.BlockDevice, |
| 738 | | os.DT_CHR => Entry.Kind.CharacterDevice, |
| 739 | | os.DT_DIR => Entry.Kind.Directory, |
| 740 | | os.DT_FIFO => Entry.Kind.NamedPipe, |
| 741 | | os.DT_LNK => Entry.Kind.SymLink, |
| 742 | | os.DT_REG => Entry.Kind.File, |
| 743 | | os.DT_SOCK => Entry.Kind.UnixDomainSocket, |
| 744 | | os.DT_WHT => Entry.Kind.Whiteout, |
| 745 | | else => Entry.Kind.Unknown, |
| 746 | | }; |
| 747 | | return Entry{ |
| 748 | | .name = name, |
| 749 | | .kind = entry_kind, |
| 777 | error.AccessDenied, |
| 778 | error.SymLinkLoop, |
| 779 | error.ProcessFdQuotaExceeded, |
| 780 | error.NameTooLong, |
| 781 | error.SystemFdQuotaExceeded, |
| 782 | error.NoDevice, |
| 783 | error.SystemResources, |
| 784 | error.Unexpected, |
| 785 | error.InvalidUtf8, |
| 786 | error.BadPathName, |
| 787 | error.DeviceBusy, |
| 788 | => |e| return e, |
| 750 | 789 | }; |
| 790 | var cleanup_dir_parent: ?Dir = null; |
| 791 | defer if (cleanup_dir_parent) |*d| d.close(); |
| 792 | |
| 793 | var cleanup_dir = true; |
| 794 | defer if (cleanup_dir) dir.close(); |
| 795 | |
| 796 | var dir_name_buf: [MAX_PATH_BYTES]u8 = undefined; |
| 797 | var dir_name: []const u8 = sub_path; |
| 798 | var parent_dir = self; |
| 799 | |
| 800 | // Here we must avoid recursion, in order to provide O(1) memory guarantee of this function. |
| 801 | // Go through each entry and if it is not a directory, delete it. If it is a directory, |
| 802 | // open it, and close the original directory. Repeat. Then start the entire operation over. |
| 803 | |
| 804 | scan_dir: while (true) { |
| 805 | var dir_it = dir.iterate(); |
| 806 | while (try dir_it.next()) |entry| { |
| 807 | if (dir.deleteFile(entry.name)) { |
| 808 | continue; |
| 809 | } else |err| switch (err) { |
| 810 | error.FileNotFound => continue, |
| 811 | |
| 812 | // Impossible because we do not pass any path separators. |
| 813 | error.NotDir => unreachable, |
| 814 | |
| 815 | error.IsDir => {}, |
| 816 | error.AccessDenied => got_access_denied = true, |
| 817 | |
| 818 | error.InvalidUtf8, |
| 819 | error.SymLinkLoop, |
| 820 | error.NameTooLong, |
| 821 | error.SystemResources, |
| 822 | error.ReadOnlyFileSystem, |
| 823 | error.FileSystem, |
| 824 | error.FileBusy, |
| 825 | error.BadPathName, |
| 826 | error.Unexpected, |
| 827 | => |e| return e, |
| 828 | } |
| 829 | |
| 830 | const new_dir = dir.openDir(entry.name) catch |err| switch (err) { |
| 831 | error.NotDir => { |
| 832 | if (got_access_denied) { |
| 833 | return error.AccessDenied; |
| 834 | } |
| 835 | continue :scan_dir; |
| 836 | }, |
| 837 | error.FileNotFound => { |
| 838 | // That's fine, we were trying to remove this directory anyway. |
| 839 | continue :scan_dir; |
| 840 | }, |
| 841 | |
| 842 | error.AccessDenied, |
| 843 | error.SymLinkLoop, |
| 844 | error.ProcessFdQuotaExceeded, |
| 845 | error.NameTooLong, |
| 846 | error.SystemFdQuotaExceeded, |
| 847 | error.NoDevice, |
| 848 | error.SystemResources, |
| 849 | error.Unexpected, |
| 850 | error.InvalidUtf8, |
| 851 | error.BadPathName, |
| 852 | error.DeviceBusy, |
| 853 | => |e| return e, |
| 854 | }; |
| 855 | if (cleanup_dir_parent) |*d| d.close(); |
| 856 | cleanup_dir_parent = dir; |
| 857 | dir = new_dir; |
| 858 | mem.copy(u8, &dir_name_buf, entry.name); |
| 859 | dir_name = dir_name_buf[0..entry.name.len]; |
| 860 | continue :scan_dir; |
| 861 | } |
| 862 | // Reached the end of the directory entries, which means we successfully deleted all of them. |
| 863 | // Now to remove the directory itself. |
| 864 | dir.close(); |
| 865 | cleanup_dir = false; |
| 866 | |
| 867 | if (cleanup_dir_parent) |d| { |
| 868 | d.deleteDir(dir_name) catch |err| switch (err) { |
| 869 | // These two things can happen due to file system race conditions. |
| 870 | error.FileNotFound, error.DirNotEmpty => continue :start_over, |
| 871 | else => |e| return e, |
| 872 | }; |
| 873 | continue :start_over; |
| 874 | } else { |
| 875 | self.deleteDir(sub_path) catch |err| switch (err) { |
| 876 | error.FileNotFound => return, |
| 877 | error.DirNotEmpty => continue :start_over, |
| 878 | else => |e| return e, |
| 879 | }; |
| 880 | return; |
| 881 | } |
| 882 | } |
| 751 | 883 | } |
| 752 | 884 | } |
| 753 | 885 | }; |
| ... | ... | @@ -757,13 +889,18 @@ pub const Walker = struct { |
| 757 | 889 | name_buffer: std.Buffer, |
| 758 | 890 | |
| 759 | 891 | pub const Entry = struct { |
| 760 | | path: []const u8, |
| 892 | /// The containing directory. This can be used to operate directly on `basename` |
| 893 | /// rather than `path`, avoiding `error.NameTooLong` for deeply nested paths. |
| 894 | /// The directory remains open until `next` or `deinit` is called. |
| 895 | dir: Dir, |
| 761 | 896 | basename: []const u8, |
| 897 | |
| 898 | path: []const u8, |
| 762 | 899 | kind: Dir.Entry.Kind, |
| 763 | 900 | }; |
| 764 | 901 | |
| 765 | 902 | const StackItem = struct { |
| 766 | | dir_it: Dir, |
| 903 | dir_it: Dir.Iterator, |
| 767 | 904 | dirname_len: usize, |
| 768 | 905 | }; |
| 769 | 906 | |
| ... | ... | @@ -781,23 +918,26 @@ pub const Walker = struct { |
| 781 | 918 | try self.name_buffer.appendByte(path.sep); |
| 782 | 919 | try self.name_buffer.append(base.name); |
| 783 | 920 | if (base.kind == .Directory) { |
| 784 | | // TODO https://github.com/ziglang/zig/issues/2888 |
| 785 | | var new_dir = try Dir.open(self.name_buffer.toSliceConst()); |
| 921 | var new_dir = top.dir_it.dir.openDir(base.name) catch |err| switch (err) { |
| 922 | error.NameTooLong => unreachable, // no path sep in base.name |
| 923 | else => |e| return e, |
| 924 | }; |
| 786 | 925 | { |
| 787 | 926 | errdefer new_dir.close(); |
| 788 | 927 | try self.stack.append(StackItem{ |
| 789 | | .dir_it = new_dir, |
| 928 | .dir_it = new_dir.iterate(), |
| 790 | 929 | .dirname_len = self.name_buffer.len(), |
| 791 | 930 | }); |
| 792 | 931 | } |
| 793 | 932 | } |
| 794 | 933 | return Entry{ |
| 934 | .dir = top.dir_it.dir, |
| 795 | 935 | .basename = self.name_buffer.toSliceConst()[dirname_len + 1 ..], |
| 796 | 936 | .path = self.name_buffer.toSliceConst(), |
| 797 | 937 | .kind = base.kind, |
| 798 | 938 | }; |
| 799 | 939 | } else { |
| 800 | | self.stack.pop().dir_it.close(); |
| 940 | self.stack.pop().dir_it.dir.close(); |
| 801 | 941 | } |
| 802 | 942 | } |
| 803 | 943 | } |
| ... | ... | @@ -812,12 +952,13 @@ pub const Walker = struct { |
| 812 | 952 | /// Recursively iterates over a directory. |
| 813 | 953 | /// Must call `Walker.deinit` when done. |
| 814 | 954 | /// `dir_path` must not end in a path separator. |
| 955 | /// The order of returned file system entries is undefined. |
| 815 | 956 | /// TODO: https://github.com/ziglang/zig/issues/2888 |
| 816 | 957 | pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker { |
| 817 | 958 | assert(!mem.endsWith(u8, dir_path, path.sep_str)); |
| 818 | 959 | |
| 819 | | var dir_it = try Dir.open(dir_path); |
| 820 | | errdefer dir_it.close(); |
| 960 | var dir = try Dir.open(dir_path); |
| 961 | errdefer dir.close(); |
| 821 | 962 | |
| 822 | 963 | var name_buffer = try std.Buffer.init(allocator, dir_path); |
| 823 | 964 | errdefer name_buffer.deinit(); |
| ... | ... | @@ -828,7 +969,7 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker { |
| 828 | 969 | }; |
| 829 | 970 | |
| 830 | 971 | try walker.stack.append(Walker.StackItem{ |
| 831 | | .dir_it = dir_it, |
| 972 | .dir_it = dir.iterate(), |
| 832 | 973 | .dirname_len = dir_path.len, |
| 833 | 974 | }); |
| 834 | 975 | |