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
8080pub extern "c" fn munmap(addr: *align(page_size) c_void, len: usize) c_int;
8181pub extern "c" fn mprotect(addr: *align(page_size) c_void, len: usize, prot: c_uint) c_int;
8282pub 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;
8384pub extern "c" fn getcwd(buf: [*]u8, size: usize) ?[*]u8;
8485pub extern "c" fn waitpid(pid: c_int, stat_loc: *c_uint, options: c_uint) c_int;
8586pub extern "c" fn fork() c_int;
lib/std/fs.zig+493-352
......@@ -37,8 +37,6 @@ pub const MAX_PATH_BYTES = switch (builtin.os) {
3737 else => @compileError("Unsupported OS"),
3838};
3939
40pub const MAX_BUF_BYTES: usize = 8192;
41
4240// here we replace the standard +/ with -_ so that it can be used in a file name
4341const b64_fs_encoder = base64.Base64Encoder.init("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", base64.standard_pad_char);
4442
......@@ -337,136 +335,35 @@ pub fn deleteDirW(dir_path: [*]const u16) !void {
337335 return os.rmdirW(dir_path);
338336}
339337
340const 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
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);
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.
343pub 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);
440357 }
441358}
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.
445360pub const Dir = struct {
446 handle: Handle,
361 fd: os.fd_t,
447362
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 };
470367
471368 pub const Entry = struct {
472369 name: []const u8,
......@@ -485,269 +382,504 @@ pub const Dir = struct {
485382 };
486383 };
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
488580 pub const OpenError = error{
489581 FileNotFound,
490582 NotDir,
491583 AccessDenied,
492 FileTooBig,
493 IsDir,
494584 SymLinkLoop,
495585 ProcessFdQuotaExceeded,
496586 NameTooLong,
497587 SystemFdQuotaExceeded,
498588 NoDevice,
499589 SystemResources,
500 NoSpaceLeft,
501 PathAlreadyExists,
502 OutOfMemory,
503590 InvalidUtf8,
504591 BadPathName,
505592 DeviceBusy,
593 } || os.UnexpectedError;
506594
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.
513596 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);
542598 }
543599
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);
549603 }
550604
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");
561608 }
609 os.close(self.fd);
610 self.* = undefined;
562611 }
563612
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);
566616 return self.openReadC(&path_c);
567617 }
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 {
570621 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);
572623 return File.openHandle(fd);
573624 }
574625
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 }
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, "..")) {
607 continue :start_over;
608 }
645 pub const DeleteFileError = os.UnlinkError;
609646
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);
626651 }
627652
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 };
653659 }
654660
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 };
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 .. 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 }
684691
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"),
699709 }
700710 }
701711
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,
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, "..")) {
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,
734764 }
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) {
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,
750789 };
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 }
751883 }
752884 }
753885};
......@@ -757,13 +889,18 @@ pub const Walker = struct {
757889 name_buffer: std.Buffer,
758890
759891 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,
761896 basename: []const u8,
897
898 path: []const u8,
762899 kind: Dir.Entry.Kind,
763900 };
764901
765902 const StackItem = struct {
766 dir_it: Dir,
903 dir_it: Dir.Iterator,
767904 dirname_len: usize,
768905 };
769906
......@@ -781,23 +918,26 @@ pub const Walker = struct {
781918 try self.name_buffer.appendByte(path.sep);
782919 try self.name_buffer.append(base.name);
783920 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 };
786925 {
787926 errdefer new_dir.close();
788927 try self.stack.append(StackItem{
789 .dir_it = new_dir,
928 .dir_it = new_dir.iterate(),
790929 .dirname_len = self.name_buffer.len(),
791930 });
792931 }
793932 }
794933 return Entry{
934 .dir = top.dir_it.dir,
795935 .basename = self.name_buffer.toSliceConst()[dirname_len + 1 ..],
796936 .path = self.name_buffer.toSliceConst(),
797937 .kind = base.kind,
798938 };
799939 } else {
800 self.stack.pop().dir_it.close();
940 self.stack.pop().dir_it.dir.close();
801941 }
802942 }
803943 }
......@@ -812,12 +952,13 @@ pub const Walker = struct {
812952/// Recursively iterates over a directory.
813953/// Must call `Walker.deinit` when done.
814954/// `dir_path` must not end in a path separator.
955/// The order of returned file system entries is undefined.
815956/// TODO: https://github.com/ziglang/zig/issues/2888
816957pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {
817958 assert(!mem.endsWith(u8, dir_path, path.sep_str));
818959
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();
821962
822963 var name_buffer = try std.Buffer.init(allocator, dir_path);
823964 errdefer name_buffer.deinit();
......@@ -828,7 +969,7 @@ pub fn walkPath(allocator: *Allocator, dir_path: []const u8) !Walker {
828969 };
829970
830971 try walker.stack.append(Walker.StackItem{
831 .dir_it = dir_it,
972 .dir_it = dir.iterate(),
832973 .dirname_len = dir_path.len,
833974 });
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
529529
530530pub const OpenError = error{
531531 AccessDenied,
532 FileTooBig,
533 IsDir,
534532 SymLinkLoop,
535533 ProcessFdQuotaExceeded,
536 NameTooLong,
537534 SystemFdQuotaExceeded,
538535 NoDevice,
539536 FileNotFound,
540537
538 /// The path exceeded `MAX_PATH_BYTES` bytes.
539 NameTooLong,
540
541541 /// Insufficient kernel memory was available, or
542542 /// the named file is a FIFO and per-user hard limit on
543543 /// memory allocation for pipes has been reached.
544544 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.
546555 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.
547559 NotDir,
560
561 /// The path already exists and the `O_CREAT` and `O_EXCL` flags were provided.
548562 PathAlreadyExists,
549563 DeviceBusy,
550564} || UnexpectedError;
......@@ -978,6 +992,42 @@ pub fn unlinkC(file_path: [*]const u8) UnlinkError!void {
978992 }
979993}
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
9811031const RenameError = error{
9821032 AccessDenied,
9831033 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
286286 var dir = try fs.Dir.open(file_path);
287287 defer dir.close();
288288
289 while (try dir.next()) |entry| {
290 if (entry.kind == fs.Dir.Entry.Kind.Directory or mem.endsWith(u8, entry.name, ".zig")) {
289 var dir_it = dir.iterate();
290
291 while (try dir_it.next()) |entry| {
292 if (entry.kind == .Directory or mem.endsWith(u8, entry.name, ".zig")) {
291293 const full_path = try fs.path.join(fmt.allocator, [_][]const u8{ file_path, entry.name });
292294 try fmtPath(fmt, full_path, check_mode);
293295 }