diff options
Diffstat (limited to 'src/bun.js/node/dir_iterator.zig')
-rw-r--r-- | src/bun.js/node/dir_iterator.zig | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig new file mode 100644 index 000000000..19db4177d --- /dev/null +++ b/src/bun.js/node/dir_iterator.zig @@ -0,0 +1,347 @@ +// This is copied from std.fs.Dir.Iterator +// The differences are: +// - it returns errors in the expected format +// - doesn't mark BADF as unreachable +// - It uses PathString instead of []const u8 + +const builtin = @import("builtin"); +const std = @import("std"); +const os = std.os; + +const Dir = std.fs.Dir; +const JSC = @import("../../jsc.zig"); +const PathString = JSC.PathString; + +const IteratorError = error{ AccessDenied, SystemResources } || os.UnexpectedError; +const mem = std.mem; +const strings = @import("../../global.zig").strings; +const Maybe = JSC.Maybe; +const File = std.fs.File; +const Result = Maybe(?Entry); + +const Entry = JSC.Node.DirEnt; + +pub const Iterator = switch (builtin.os.tag) { + .macos, .ios, .freebsd, .netbsd, .dragonfly, .openbsd, .solaris => struct { + dir: Dir, + seek: i64, + buf: [8192]u8, // TODO align(@alignOf(os.system.dirent)), + index: usize, + end_index: usize, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + const next = switch (builtin.os.tag) { + .macos, .ios => nextDarwin, + // .freebsd, .netbsd, .dragonfly, .openbsd => nextBsd, + // .solaris => nextSolaris, + else => @compileError("unimplemented"), + }; + + fn nextDarwin(self: *Self) Result { + start_over: while (true) { + if (self.index >= self.end_index) { + const rc = os.system.__getdirentries64( + self.dir.fd, + &self.buf, + self.buf.len, + &self.seek, + ); + + if (rc < 1) { + if (rc == 0) return Result{ .result = null }; + if (Result.errnoSys(rc, .getdirentries64)) |err| { + return err; + } + } + + self.index = 0; + self.end_index = @intCast(usize, rc); + } + const darwin_entry = @ptrCast(*align(1) os.system.dirent, &self.buf[self.index]); + const next_index = self.index + darwin_entry.reclen(); + self.index = next_index; + + const name = @ptrCast([*]u8, &darwin_entry.d_name)[0..darwin_entry.d_namlen]; + + if (strings.eqlComptime(name, ".") or strings.eqlComptime(name, "..") or (darwin_entry.d_ino == 0)) { + continue :start_over; + } + + const entry_kind = switch (darwin_entry.d_type) { + os.DT.BLK => Entry.Kind.BlockDevice, + os.DT.CHR => Entry.Kind.CharacterDevice, + os.DT.DIR => Entry.Kind.Directory, + os.DT.FIFO => Entry.Kind.NamedPipe, + os.DT.LNK => Entry.Kind.SymLink, + os.DT.REG => Entry.Kind.File, + os.DT.SOCK => Entry.Kind.UnixDomainSocket, + os.DT.WHT => Entry.Kind.Whiteout, + else => Entry.Kind.Unknown, + }; + return .{ + .result = Entry{ + .name = PathString.init(name), + .kind = entry_kind, + }, + }; + } + } + }, + + .linux => struct { + dir: Dir, + // The if guard is solely there to prevent compile errors from missing `linux.dirent64` + // definition when compiling for other OSes. It doesn't do anything when compiling for Linux. + buf: [8192]u8 align(if (builtin.os.tag != .linux) 1 else @alignOf(linux.dirent64)), + index: usize, + end_index: usize, + + const Self = @This(); + const linux = os.linux; + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Result { + start_over: while (true) { + if (self.index >= self.end_index) { + const rc = linux.getdents64(self.dir.fd, &self.buf, self.buf.len); + if (Result.errnoSys(rc, .getdents64)) |err| return err; + if (rc == 0) return .{ .result = null }; + self.index = 0; + self.end_index = rc; + } + const linux_entry = @ptrCast(*align(1) linux.dirent64, &self.buf[self.index]); + const next_index = self.index + linux_entry.reclen(); + self.index = next_index; + + const name = mem.sliceTo(@ptrCast([*:0]u8, &linux_entry.d_name), 0); + + // skip . and .. entries + if (strings.eqlComptime(name, ".") or strings.eqlComptime(name, "..")) { + continue :start_over; + } + + const entry_kind = switch (linux_entry.d_type) { + linux.DT.BLK => Entry.Kind.BlockDevice, + linux.DT.CHR => Entry.Kind.CharacterDevice, + linux.DT.DIR => Entry.Kind.Directory, + linux.DT.FIFO => Entry.Kind.NamedPipe, + linux.DT.LNK => Entry.Kind.SymLink, + linux.DT.REG => Entry.Kind.File, + linux.DT.SOCK => Entry.Kind.UnixDomainSocket, + else => Entry.Kind.Unknown, + }; + return .{ + .result = Entry{ + .name = PathString.init(name), + .kind = entry_kind, + }, + }; + } + } + }, + .windows => struct { + dir: Dir, + buf: [8192]u8 align(@alignOf(os.windows.FILE_BOTH_DIR_INFORMATION)), + index: usize, + end_index: usize, + first: bool, + name_data: [256]u8, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Result { + while (true) { + const w = os.windows; + if (self.index >= self.end_index) { + var io: w.IO_STATUS_BLOCK = undefined; + const rc = w.ntdll.NtQueryDirectoryFile( + self.dir.fd, + null, + null, + null, + &io, + &self.buf, + self.buf.len, + .FileBothDirectoryInformation, + w.FALSE, + null, + if (self.first) @as(w.BOOLEAN, w.TRUE) else @as(w.BOOLEAN, w.FALSE), + ); + self.first = false; + if (io.Information == 0) return .{ .result = null }; + self.index = 0; + self.end_index = io.Information; + switch (rc) { + .SUCCESS => {}, + .ACCESS_DENIED => return error.AccessDenied, // Double-check that the Dir was opened with iteration ability + + else => return w.unexpectedStatus(rc), + } + } + + const aligned_ptr = @alignCast(@alignOf(w.FILE_BOTH_DIR_INFORMATION), &self.buf[self.index]); + const dir_info = @ptrCast(*w.FILE_BOTH_DIR_INFORMATION, aligned_ptr); + if (dir_info.NextEntryOffset != 0) { + self.index += dir_info.NextEntryOffset; + } else { + self.index = self.buf.len; + } + + const name_utf16le = @ptrCast([*]u16, &dir_info.FileName)[0 .. dir_info.FileNameLength / 2]; + + if (mem.eql(u16, name_utf16le, &[_]u16{'.'}) or mem.eql(u16, name_utf16le, &[_]u16{ '.', '.' })) + continue; + // Trust that Windows gives us valid UTF-16LE + const name_utf8_len = std.unicode.utf16leToUtf8(self.name_data[0..], name_utf16le) catch unreachable; + const name_utf8 = self.name_data[0..name_utf8_len]; + const kind = blk: { + const attrs = dir_info.FileAttributes; + if (attrs & w.FILE_ATTRIBUTE_DIRECTORY != 0) break :blk Entry.Kind.Directory; + if (attrs & w.FILE_ATTRIBUTE_REPARSE_POINT != 0) break :blk Entry.Kind.SymLink; + break :blk Entry.Kind.File; + }; + return .{ + .result = Entry{ + .name = PathString.init(name_utf8), + .kind = kind, + }, + }; + } + } + }, + .wasi => struct { + dir: Dir, + buf: [8192]u8, // TODO align(@alignOf(os.wasi.dirent_t)), + cookie: u64, + index: usize, + end_index: usize, + + const Self = @This(); + + pub const Error = IteratorError; + + /// Memory such as file names referenced in this returned entry becomes invalid + /// with subsequent calls to `next`, as well as when this `Dir` is deinitialized. + pub fn next(self: *Self) Result { + // We intentinally use fd_readdir even when linked with libc, + // since its implementation is exactly the same as below, + // and we avoid the code complexity here. + const w = os.wasi; + start_over: while (true) { + if (self.index >= self.end_index) { + var bufused: usize = undefined; + switch (w.fd_readdir(self.dir.fd, &self.buf, self.buf.len, self.cookie, &bufused)) { + .SUCCESS => {}, + .BADF => unreachable, // Dir is invalid or was opened without iteration ability + .FAULT => unreachable, + .NOTDIR => unreachable, + .INVAL => unreachable, + .NOTCAPABLE => return error.AccessDenied, + else => |err| return os.unexpectedErrno(err), + } + if (bufused == 0) return null; + self.index = 0; + self.end_index = bufused; + } + const entry = @ptrCast(*align(1) w.dirent_t, &self.buf[self.index]); + const entry_size = @sizeOf(w.dirent_t); + const name_index = self.index + entry_size; + const name = mem.span(self.buf[name_index .. name_index + entry.d_namlen]); + + const next_index = name_index + entry.d_namlen; + self.index = next_index; + self.cookie = entry.d_next; + + // skip . and .. entries + if (strings.eqlComptime(name, ".") or strings.eqlComptime(name, "..")) { + continue :start_over; + } + + const entry_kind = switch (entry.d_type) { + .BLOCK_DEVICE => Entry.Kind.BlockDevice, + .CHARACTER_DEVICE => Entry.Kind.CharacterDevice, + .DIRECTORY => Entry.Kind.Directory, + .SYMBOLIC_LINK => Entry.Kind.SymLink, + .REGULAR_FILE => Entry.Kind.File, + .SOCKET_STREAM, .SOCKET_DGRAM => Entry.Kind.UnixDomainSocket, + else => Entry.Kind.Unknown, + }; + return Entry{ + .name = name, + .kind = entry_kind, + }; + } + } + }, + else => @compileError("unimplemented"), +}; + +const WrappedIterator = struct { + iter: Iterator, + const Self = @This(); + + pub const Error = IteratorError; + + pub inline fn next(self: *Self) Result { + return self.iter.next(); + } +}; + +pub fn iterate(self: Dir) WrappedIterator { + return WrappedIterator{ + .iter = _iterate(self), + }; +} + +fn _iterate(self: Dir) Iterator { + switch (builtin.os.tag) { + .macos, + .ios, + .freebsd, + .netbsd, + .dragonfly, + .openbsd, + .solaris, + => return Iterator{ + .dir = self, + .seek = 0, + .index = 0, + .end_index = 0, + .buf = undefined, + }, + .linux, .haiku => return Iterator{ + .dir = self, + .index = 0, + .end_index = 0, + .buf = undefined, + }, + .windows => return Iterator{ + .dir = self, + .index = 0, + .end_index = 0, + .first = true, + .buf = undefined, + .name_data = undefined, + }, + .wasi => return Iterator{ + .dir = self, + .cookie = os.wasi.DIRCOOKIE_START, + .index = 0, + .end_index = 0, + .buf = undefined, + }, + else => @compileError("unimplemented"), + } +} |