authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-10-20 21:48:23-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-10-20 21:48:23-04:00
log5b1a492012241276a4b7539ca6664234f0629c79
tree1f3d38be4e73ab30d65efc453892c355ef7d3ec4
parente78d3750c58d26bac0e24c40eb89c2f4796bc15c
signaturelock-open Commit is signed but in an unrecognized format.

breaking: improve std.fs directory handling API

* Added `std.c.unlinkat` and `std.os.unlinkat`. * Removed `std.fs.MAX_BUF_BYTES` (this declaration never made it to master branch) * Added `std.fs.Dir.deleteTree` to be used on an open directory handle. * `std.fs.deleteTree` has better behavior for both relative and absolute paths. For absolute paths, it opens the base directory and uses that handle for subsequent operations. For relative paths, it does a similar strategy, using the cwd handle. * The error set of `std.fs.deleteTree` is improved to no longer have these possible errors: - OutOfMemory - FileTooBig - IsDir - DirNotEmpty - PathAlreadyExists - NoSpaceLeft * Added `std.fs.Dir.posix_cwd` which is a statically initialized directory representing the current working directory. * The error set of `std.Dir.open` is improved to no longer have these possible errors: - FileTooBig - IsDir - NoSpaceLeft - PathAlreadyExists - OutOfMemory * Added more alternative functions to `std.fs` for when the path parameter is a null terminated string. This can sometimes be more effecient on systems which have an ABI based on null terminated strings. * Added `std.fs.Dir.openDir`, `std.fs.Dir.deleteFile`, and `std.fs.Dir.deleteDir` which all operate on an open directory handle. * `std.fs.Walker.Entry` now has a `dir` field, which can be used to do operations directly on `std.fs.Walker.Entry.basename`, avoiding `error.NameTooLong` for deeply nested paths. * Added more docs to `std.os.OpenError` This commit does the POSIX components for these changes. I plan to follow up shortly with a commit for Windows.

4 files changed, 551 insertions(+), 357 deletions(-)

lib/std/c.zig+1
...@@ -80,6 +80,7 @@ pub extern "c" fn mmap(addr: ?*align(page_size) c_void, len: usize, prot: c_uint...@@ -80,6 +80,7 @@ pub extern "c" fn mmap(addr: ?*align(page_size) c_void, len: usize, prot: c_uint
80pub extern "c" fn munmap(addr: *align(page_size) c_void, len: usize) c_int;80pub extern "c" fn munmap(addr: *align(page_size) c_void, len: usize) c_int;
81pub extern "c" fn mprotect(addr: *align(page_size) c_void, len: usize, prot: c_uint) c_int;81pub extern "c" fn mprotect(addr: *align(page_size) c_void, len: usize, prot: c_uint) c_int;
82pub extern "c" fn unlink(path: [*]const u8) c_int;82pub extern "c" fn unlink(path: [*]const u8) c_int;
83pub extern "c" fn unlinkat(dirfd: fd_t, path: [*]const u8, flags: c_uint) c_int;
83pub extern "c" fn getcwd(buf: [*]u8, size: usize) ?[*]u8;84pub extern "c" fn getcwd(buf: [*]u8, size: usize) ?[*]u8;
84pub extern "c" fn waitpid(pid: c_int, stat_loc: *c_uint, options: c_uint) c_int;85pub extern "c" fn waitpid(pid: c_int, stat_loc: *c_uint, options: c_uint) c_int;
85pub extern "c" fn fork() c_int;86pub extern "c" fn fork() c_int;
lib/std/fs.zig+493-352
...@@ -37,8 +37,6 @@ pub const MAX_PATH_BYTES = switch (builtin.os) {...@@ -37,8 +37,6 @@ pub const MAX_PATH_BYTES = switch (builtin.os) {
37 else => @compileError("Unsupported OS"),37 else => @compileError("Unsupported OS"),
38};38};
3939
40pub const MAX_BUF_BYTES: usize = 8192;
41
42// here we replace the standard +/ with -_ so that it can be used in a file name40// here we replace the standard +/ with -_ so that it can be used in a file name
43const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char);41const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char);
4442
...@@ -337,136 +335,35 @@ pub fn deleteDirW(dir_path: [*]const u16) !void {...@@ -337,136 +335,35 @@ pub fn deleteDirW(dir_path: [*]const u16) !void {
337 return os.rmdirW(dir_path);335 return os.rmdirW(dir_path);
338}336}
339337
340const DeleteTreeError = error{338/// Removes a symlink, file, or directory.
341 OutOfMemory,339/// If `full_path` is relative, this is equivalent to `Dir.deleteTree` with the
342 AccessDenied,340/// current working directory as the open directory handle.
343 FileTooBig,341/// If `full_path` is absolute, this is equivalent to `Dir.deleteTree` with the
344 IsDir,342/// base directory.
345 SymLinkLoop,343pub fn deleteTree(full_path: []const u8) !void {
346 ProcessFdQuotaExceeded,344 if (path.isAbsolute(full_path)) {
347 NameTooLong,345 const dirname = path.dirname(full_path) orelse return error{
348 SystemFdQuotaExceeded,346 /// Attempt to remove the root file system path.
349 NoDevice,347 /// This error is unreachable if `full_path` is relative.
350 SystemResources,348 CannotDeleteRootDirectory,
351 NoSpaceLeft,349 }.CannotDeleteRootDirectory;
352 PathAlreadyExists,350
353 ReadOnlyFileSystem,351 var dir = try Dir.open(dirname);
354 NotDir,352 defer dir.close();
355 FileNotFound,353
356 FileSystem,354 return dir.deleteTree(path.basename(full_path));
357 FileBusy,355 } else {
358 DirNotEmpty,356 return Dir.posix_cwd.deleteTree(full_path);
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
376pub 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);
440 }357 }
441}358}
442359
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.
445pub const Dir = struct {360pub const Dir = struct {
446 handle: Handle,361 fd: os.fd_t,
447362
448 pub const Handle = switch (builtin.os) {363 /// An open handle to the current working directory.
449 .macosx, .ios, .freebsd, .netbsd => struct {364 /// Closing this directory is safety-checked illegal behavior.
450 fd: i32,365 /// Not available on Windows.
451 seek: i64,366 pub const posix_cwd = Dir{ .fd = os.AT_FDCWD };
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 };
470367
471 pub const Entry = struct {368 pub const Entry = struct {
472 name: []const u8,369 name: []const u8,
...@@ -485,269 +382,504 @@ pub const Dir = struct {...@@ -485,269 +382,504 @@ pub const Dir = struct {
485 };382 };
486 };383 };
487384
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 pub const OpenError = error{580 pub const OpenError = error{
489 FileNotFound,581 FileNotFound,
490 NotDir,582 NotDir,
491 AccessDenied,583 AccessDenied,
492 FileTooBig,
493 IsDir,
494 SymLinkLoop,584 SymLinkLoop,
495 ProcessFdQuotaExceeded,585 ProcessFdQuotaExceeded,
496 NameTooLong,586 NameTooLong,
497 SystemFdQuotaExceeded,587 SystemFdQuotaExceeded,
498 NoDevice,588 NoDevice,
499 SystemResources,589 SystemResources,
500 NoSpaceLeft,
501 PathAlreadyExists,
502 OutOfMemory,
503 InvalidUtf8,590 InvalidUtf8,
504 BadPathName,591 BadPathName,
505 DeviceBusy,592 DeviceBusy,
593 } || os.UnexpectedError;
506594
507 Unexpected,595 /// Call `close` to free the directory handle.
508 };
509
510 /// Call close when done.
511 /// TODO remove the allocator requirement from this API
512 /// https://github.com/ziglang/zig/issues/2885
513 pub fn open(dir_path: []const u8) OpenError!Dir {596 pub fn open(dir_path: []const u8) OpenError!Dir {
514 return Dir{597 return posix_cwd.openDir(dir_path);
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 };
542 }598 }
543599
544 pub fn close(self: *Dir) void {600 /// Same as `open` except the parameter is null-terminated.
545 if (os.windows.is_the_target) {601 pub fn openC(dir_path_c: [*]const u8) OpenError!Dir {
546 return os.windows.FindClose(self.handle.handle);602 return posix_cwd.openDirC(dir_path_c);
547 }
548 os.close(self.handle.fd);
549 }603 }
550604
551 /// Memory such as file names referenced in this returned entry becomes invalid605 pub fn close(self: *Dir) void {
552 /// with subsequent calls to next, as well as when this `Dir` is deinitialized.606 if (os.windows.is_the_target) {
553 pub fn next(self: *Dir) !?Entry {607 @panic("TODO");
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"),
561 }608 }
609 os.close(self.fd);
610 self.* = undefined;
562 }611 }
563612
564 pub fn openRead(self: Dir, file_path: []const u8) os.OpenError!File {613 /// Call `File.close` on the result when done.
565 const path_c = try os.toPosixPath(file_path);614 pub fn openRead(self: Dir, sub_path: []const u8) File.OpenError!File {
615 const path_c = try os.toPosixPath(sub_path);
566 return self.openReadC(&path_c);616 return self.openReadC(&path_c);
567 }617 }
568618
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 const flags = os.O_LARGEFILE | os.O_RDONLY;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 return File.openHandle(fd);623 return File.openHandle(fd);
573 }624 }
574625
575 fn nextDarwin(self: *Dir) !?Entry {626 /// Call `close` on the result when done.
576 start_over: while (true) {627 pub fn openDir(self: Dir, sub_path: []const u8) OpenError!Dir {
577 if (self.handle.index >= self.handle.end_index) {628 const sub_path_c = try os.toPosixPath(sub_path);
578 while (true) {629 return self.openDirC(&sub_path_c);
579 const rc = os.system.__getdirentries64(630 }
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;
603631
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 }
605644
606 if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {645 pub const DeleteFileError = os.UnlinkError;
607 continue :start_over;
608 }
609646
610 const entry_kind = switch (darwin_entry.d_type) {647 /// Delete a file name and possibly the file it refers to, based on an open directory handle.
611 os.DT_BLK => Entry.Kind.BlockDevice,648 pub fn deleteFile(self: Dir, sub_path: []const u8) DeleteFileError!void {
612 os.DT_CHR => Entry.Kind.CharacterDevice,649 const sub_path_c = try os.toPosixPath(sub_path);
613 os.DT_DIR => Entry.Kind.Directory,650 return self.deleteFileC(&sub_path_c);
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 }
626 }651 }
627652
628 fn nextWindows(self: *Dir) !?Entry {653 /// Same as `deleteFile` except the parameter is null-terminated.
629 while (true) {654 pub fn deleteFileC(self: Dir, sub_path_c: [*]const u8) DeleteFileError!void {
630 if (self.handle.first) {655 os.unlinkatC(self.fd, sub_path_c, 0) catch |err| switch (err) {
631 self.handle.first = false;656 error.DirNotEmpty => unreachable, // not passing AT_REMOVEDIR
632 } else {657 else => |e| return e,
633 if (!try os.windows.FindNextFile(self.handle.handle, &self.handle.find_file_data))658 };
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 }659 }
654660
655 fn nextLinux(self: *Dir) !?Entry {661 pub const DeleteDirError = error{
656 start_over: while (true) {662 DirNotEmpty,
657 if (self.handle.index >= self.handle.end_index) {663 FileNotFound,
658 while (true) {664 AccessDenied,
659 const rc = os.linux.getdents64(self.handle.fd, self.handle.buf[0..].ptr, self.handle.buf.len);665 FileBusy,
660 switch (os.linux.getErrno(rc)) {666 FileSystem,
661 0 => {},667 SymLinkLoop,
662 os.EBADF => unreachable,668 NameTooLong,
663 os.EFAULT => unreachable,669 NotDir,
664 os.ENOTDIR => unreachable,670 SystemResources,
665 os.EINVAL => unreachable,671 ReadOnlyFileSystem,
666 else => |err| return os.unexpectedErrno(err),672 InvalidUtf8,
667 }673 BadPathName,
668 if (rc == 0) return null;674 Unexpected,
669 self.handle.index = 0;675 };
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;
677676
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 }
679683
680 // skip . and .. entries684 /// Same as `deleteDir` except the parameter is null-terminated.
681 if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {685 pub fn deleteDirC(self: Dir, sub_path_c: [*]const u8) DeleteDirError!void {
682 continue :start_over;686 os.unlinkatC(self.fd, sub_path_c, os.AT_REMOVEDIR) catch |err| switch (err) {
683 }687 error.IsDir => unreachable, // not possible since we pass AT_REMOVEDIR
688 else => |e| return e,
689 };
690 }
684691
685 const entry_kind = switch (linux_entry.d_type) {692 pub fn iterate(self: Dir) Iterator {
686 os.DT_BLK => Entry.Kind.BlockDevice,693 switch (builtin.os) {
687 os.DT_CHR => Entry.Kind.CharacterDevice,694 .macosx, .ios, .freebsd, .netbsd => return Iterator{
688 os.DT_DIR => Entry.Kind.Directory,695 .dir = self,
689 os.DT_FIFO => Entry.Kind.NamedPipe,696 .seek = 0,
690 os.DT_LNK => Entry.Kind.SymLink,697 .index = 0,
691 os.DT_REG => Entry.Kind.File,698 .end_index = 0,
692 os.DT_SOCK => Entry.Kind.UnixDomainSocket,699 .buf = undefined,
693 else => Entry.Kind.Unknown,700 },
694 };701 .linux => return Iterator{
695 return Entry{702 .dir = self,
696 .name = name,703 .index = 0,
697 .kind = entry_kind,704 .end_index = 0,
698 };705 .buf = undefined,
706 },
707 .windows => @panic("TODO"),
708 else => @compileError("unimplemented"),
699 }709 }
700 }710 }
701711
702 fn nextBsd(self: *Dir) !?Entry {712 pub const DeleteTreeError = error{
703 start_over: while (true) {713 AccessDenied,
704 if (self.handle.index >= self.handle.end_index) {714 FileTooBig,
705 while (true) {715 SymLinkLoop,
706 const rc = os.system.getdirentries(716 ProcessFdQuotaExceeded,
707 self.handle.fd,717 NameTooLong,
708 self.handle.buf[0..].ptr,718 SystemFdQuotaExceeded,
709 self.handle.buf.len,719 NoDevice,
710 &self.handle.seek,720 SystemResources,
711 );721 ReadOnlyFileSystem,
712 switch (os.errno(rc)) {722 FileSystem,
713 0 => {},723 FileBusy,
714 os.EBADF => unreachable,724 DeviceBusy,
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;
729725
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,
731729
732 if (mem.eql(u8, name, ".") or mem.eql(u8, name, "..")) {730 /// On Windows, file paths must be valid Unicode.
733 continue :start_over;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 },
735776
736 const entry_kind = switch (freebsd_entry.d_type) {777 error.AccessDenied,
737 os.DT_BLK => Entry.Kind.BlockDevice,778 error.SymLinkLoop,
738 os.DT_CHR => Entry.Kind.CharacterDevice,779 error.ProcessFdQuotaExceeded,
739 os.DT_DIR => Entry.Kind.Directory,780 error.NameTooLong,
740 os.DT_FIFO => Entry.Kind.NamedPipe,781 error.SystemFdQuotaExceeded,
741 os.DT_LNK => Entry.Kind.SymLink,782 error.NoDevice,
742 os.DT_REG => Entry.Kind.File,783 error.SystemResources,
743 os.DT_SOCK => Entry.Kind.UnixDomainSocket,784 error.Unexpected,
744 os.DT_WHT => Entry.Kind.Whiteout,785 error.InvalidUtf8,
745 else => Entry.Kind.Unknown,786 error.BadPathName,
746 };787 error.DeviceBusy,
747 return Entry{788 => |e| return e,
748 .name = name,
749 .kind = entry_kind,
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,13 +889,18 @@ pub const Walker = struct {
757 name_buffer: std.Buffer,889 name_buffer: std.Buffer,
758890
759 pub const Entry = struct {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 basename: []const u8,896 basename: []const u8,
897
898 path: []const u8,
762 kind: Dir.Entry.Kind,899 kind: Dir.Entry.Kind,
763 };900 };
764901
765 const StackItem = struct {902 const StackItem = struct {
766 dir_it: Dir,903 dir_it: Dir.Iterator,
767 dirname_len: usize,904 dirname_len: usize,
768 };905 };
769906
...@@ -781,23 +918,26 @@ pub const Walker = struct {...@@ -781,23 +918,26 @@ pub const Walker = struct {
781 try self.name_buffer.appendByte(path.sep);918 try self.name_buffer.appendByte(path.sep);
782 try self.name_buffer.append(base.name);919 try self.name_buffer.append(base.name);
783 if (base.kind == .Directory) {920 if (base.kind == .Directory) {
784 // TODO https://github.com/ziglang/zig/issues/2888921 var new_dir = top.dir_it.dir.openDir(base.name) catch |err| switch (err) {
785 var new_dir = try Dir.open(self.name_buffer.toSliceConst());922 error.NameTooLong => unreachable, // no path sep in base.name
923 else => |e| return e,
924 };
786 {925 {
787 errdefer new_dir.close();926 errdefer new_dir.close();
788 try self.stack.append(StackItem{927 try self.stack.append(StackItem{
789 .dir_it = new_dir,928 .dir_it = new_dir.iterate(),
790 .dirname_len = self.name_buffer.len(),929 .dirname_len = self.name_buffer.len(),
791 });930 });
792 }931 }
793 }932 }
794 return Entry{933 return Entry{
934 .dir = top.dir_it.dir,
795 .basename = self.name_buffer.toSliceConst()[dirname_len + 1 ..],935 .basename = self.name_buffer.toSliceConst()[dirname_len + 1 ..],
796 .path = self.name_buffer.toSliceConst(),936 .path = self.name_buffer.toSliceConst(),
797 .kind = base.kind,937 .kind = base.kind,
798 };938 };
799 } else {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,12 +952,13 @@ pub const Walker = struct {
812/// Recursively iterates over a directory.952/// Recursively iterates over a directory.
813/// Must call `Walker.deinit` when done.953/// Must call `Walker.deinit` when done.
814/// `dir_path` must not end in a path separator.954/// `dir_path` must not end in a path separator.
955/// The order of returned file system entries is undefined.
815/// TODO: https://github.com/ziglang/zig/issues/2888956/// TODO: https://github.com/ziglang/zig/issues/2888
816pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {957pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {
817 assert(!mem.endsWith(u8, dir_path, path.sep_str));958 assert(!mem.endsWith(u8, dir_path, path.sep_str));
818959
819 var dir_it = try Dir.open(dir_path);960 var dir = try Dir.open(dir_path);
820 errdefer dir_it.close();961 errdefer dir.close();
821962
822 var name_buffer = try std.Buffer.init(allocator, dir_path);963 var name_buffer = try std.Buffer.init(allocator, dir_path);
823 errdefer name_buffer.deinit();964 errdefer name_buffer.deinit();
...@@ -828,7 +969,7 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {...@@ -828,7 +969,7 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {
828 };969 };
829970
830 try walker.stack.append(Walker.StackItem{971 try walker.stack.append(Walker.StackItem{
831 .dir_it = dir_it,972 .dir_it = dir.iterate(),
832 .dirname_len = dir_path.len,973 .dirname_len = dir_path.len,
833 });974 });
834975
lib/std/os.zig+53-3
...@@ -529,22 +529,36 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) WriteError!void...@@ -529,22 +529,36 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) WriteError!void
529529
530pub const OpenError = error{530pub const OpenError = error{
531 AccessDenied,531 AccessDenied,
532 FileTooBig,
533 IsDir,
534 SymLinkLoop,532 SymLinkLoop,
535 ProcessFdQuotaExceeded,533 ProcessFdQuotaExceeded,
536 NameTooLong,
537 SystemFdQuotaExceeded,534 SystemFdQuotaExceeded,
538 NoDevice,535 NoDevice,
539 FileNotFound,536 FileNotFound,
540537
538 /// The path exceeded `MAX_PATH_BYTES` bytes.
539 NameTooLong,
540
541 /// Insufficient kernel memory was available, or541 /// Insufficient kernel memory was available, or
542 /// the named file is a FIFO and per-user hard limit on542 /// the named file is a FIFO and per-user hard limit on
543 /// memory allocation for pipes has been reached.543 /// memory allocation for pipes has been reached.
544 SystemResources,544 SystemResources,
545545
546 /// The file is too large to be opened. This error is unreachable
547 /// for 64-bit targets, as well as when opening directories.
548 FileTooBig,
549
550 /// The path refers to directory but the `O_DIRECTORY` flag was not provided.
551 IsDir,
552
553 /// A new path cannot be created because the device has no room for the new file.
554 /// This error is only reachable when the `O_CREAT` flag is provided.
546 NoSpaceLeft,555 NoSpaceLeft,
556
557 /// A component used as a directory in the path was not, in fact, a directory, or
558 /// `O_DIRECTORY` was specified and the path was not a directory.
547 NotDir,559 NotDir,
560
561 /// The path already exists and the `O_CREAT` and `O_EXCL` flags were provided.
548 PathAlreadyExists,562 PathAlreadyExists,
549 DeviceBusy,563 DeviceBusy,
550} || UnexpectedError;564} || UnexpectedError;
...@@ -978,6 +992,42 @@ pub fn unlinkC(file_path: [*]const u8) UnlinkError!void {...@@ -978,6 +992,42 @@ pub fn unlinkC(file_path: [*]const u8) UnlinkError!void {
978 }992 }
979}993}
980994
995pub const UnlinkatError = UnlinkError || error{
996 /// When passing `AT_REMOVEDIR`, this error occurs when the named directory is not empty.
997 DirNotEmpty,
998};
999
1000/// Delete a file name and possibly the file it refers to, based on an open directory handle.
1001pub fn unlinkat(dirfd: fd_t, file_path: []const u8, flags: u32) UnlinkatError!void {
1002 const file_path_c = try toPosixPath(file_path);
1003 return unlinkatC(dirfd, &file_path_c, flags);
1004}
1005
1006/// Same as `unlinkat` but `file_path` is a null-terminated string.
1007pub fn unlinkatC(dirfd: fd_t, file_path_c: [*]const u8, flags: u32) UnlinkatError!void {
1008 switch (errno(system.unlinkat(dirfd, file_path_c, flags))) {
1009 0 => return,
1010 EACCES => return error.AccessDenied,
1011 EPERM => return error.AccessDenied,
1012 EBUSY => return error.FileBusy,
1013 EFAULT => unreachable,
1014 EIO => return error.FileSystem,
1015 EISDIR => return error.IsDir,
1016 ELOOP => return error.SymLinkLoop,
1017 ENAMETOOLONG => return error.NameTooLong,
1018 ENOENT => return error.FileNotFound,
1019 ENOTDIR => return error.NotDir,
1020 ENOMEM => return error.SystemResources,
1021 EROFS => return error.ReadOnlyFileSystem,
1022 ENOTEMPTY => return error.DirNotEmpty,
1023
1024 EINVAL => unreachable, // invalid flags, or pathname has . as last component
1025 EBADF => unreachable, // always a race condition
1026
1027 else => |err| return unexpectedErrno(err),
1028 }
1029}
1030
981const RenameError = error{1031const RenameError = error{
982 AccessDenied,1032 AccessDenied,
983 FileBusy,1033 FileBusy,
src-self-hosted/stage1.zig+4-2
...@@ -286,8 +286,10 @@ fn fmtPath(fmt: *Fmt, file_path_ref: []const u8, check_mode: bool) FmtError!void...@@ -286,8 +286,10 @@ fn fmtPath(fmt: *Fmt, file_path_ref: []const u8, check_mode: bool) FmtError!void
286 var dir = try fs.Dir.open(file_path);286 var dir = try fs.Dir.open(file_path);
287 defer dir.close();287 defer dir.close();
288288
289 while (try dir.next()) |entry| {289 var dir_it = dir.iterate();
290 if (entry.kind == fs.Dir.Entry.Kind.Directory or mem.endsWith(u8, entry.name, ".zig")) {290
291 while (try dir_it.next()) |entry| {
292 if (entry.kind == .Directory or mem.endsWith(u8, entry.name, ".zig")) {
291 const full_path = try fs.path.join(fmt.allocator, [_][]const u8{ file_path, entry.name });293 const full_path = try fs.path.join(fmt.allocator, [_][]const u8{ file_path, entry.name });
292 try fmtPath(fmt, full_path, check_mode);294 try fmtPath(fmt, full_path, check_mode);
293 }295 }