| 1 | const std = @import("std"); |
| 2 | const builtin = @import("builtin"); |
| 3 | |
| 4 | const nfs = @import("./nfs.zig"); |
| 5 | const File = nfs.File; |
| 6 | const Dir = @This(); |
| 7 | |
| 8 | const os = builtin.target.os.tag; |
| 9 | |
| 10 | const sys = switch (os) { |
| 11 | .linux => @import("sys-linux"), |
| 12 | .macos => @import("sys-darwin"), |
| 13 | .freebsd => @import("sys-freebsd"), |
| 14 | .netbsd => @import("sys-netbsd"), |
| 15 | .openbsd => @import("sys-openbsd"), |
| 16 | else => unreachable, |
| 17 | }; |
| 18 | |
| 19 | pub const cwd = nfs.cwd; |
| 20 | |
| 21 | fd: nfs.Handle, |
| 22 | |
| 23 | // Resource allocation may fail; resource deallocation must succeed. |
| 24 | pub fn close(self: Dir) void { |
| 25 | return sys.close(@intFromEnum(self.fd)) catch if (builtin.mode == .Debug) unreachable; |
| 26 | } |
| 27 | |
| 28 | pub fn openFile(self: Dir, sub_path: [:0]const u8, flags: OpenFileFlags) !File { |
| 29 | var oflag: c_int = 0; |
| 30 | oflag |= switch (flags.mode) { |
| 31 | .read_only => sys.O.RDONLY, |
| 32 | .write_only => sys.O.WRONLY, |
| 33 | .read_write => sys.O.RDWR, |
| 34 | }; |
| 35 | if (os != .linux) oflag |= switch (flags.lock) { |
| 36 | .none => 0, |
| 37 | .shared => sys.O.SHLOCK, |
| 38 | .exclusive => sys.O.EXLOCK, |
| 39 | }; |
| 40 | oflag |= sys.O.CLOEXEC; |
| 41 | const fd = try sys.openat(@intFromEnum(self.fd), sub_path.ptr, oflag); |
| 42 | if (os == .linux) switch (flags.lock) { |
| 43 | .none => {}, |
| 44 | .shared => try sys.flock(fd, sys.LOCK.SH), |
| 45 | .exclusive => try sys.flock(fd, sys.LOCK.EX), |
| 46 | }; |
| 47 | return .{ .fd = @enumFromInt(fd) }; |
| 48 | } |
| 49 | pub fn openFileC(self: Dir, sub_path: []const u8, flags: OpenFileFlags) !File { |
| 50 | std.debug.assert(sub_path.len <= sys.PATH_MAX); |
| 51 | var buf: [sys.PATH_MAX + 1]u8 = undefined; |
| 52 | @memcpy(buf[0..sub_path.len], sub_path); |
| 53 | buf[sub_path.len] = 0; |
| 54 | return openFile(self, buf[0..sub_path.len :0], flags); |
| 55 | } |
| 56 | |
| 57 | pub const OpenFileFlags = struct { |
| 58 | mode: enum(u8) { read_only, write_only, read_write } = .read_only, |
| 59 | lock: enum(u8) { none, shared, exclusive } = .none, |
| 60 | }; |
| 61 | |
| 62 | pub fn openDir(self: Dir, sub_path: [:0]const u8, flags: OpenDirFlags) !Dir { |
| 63 | const oflag: c_int = sys.O.RDONLY | sys.O.DIRECTORY; |
| 64 | _ = flags; |
| 65 | return .{ .fd = @enumFromInt(try sys.openat(@intFromEnum(self.fd), sub_path.ptr, oflag)) }; |
| 66 | } |
| 67 | pub fn openDirC(self: Dir, sub_path: []const u8, flags: OpenDirFlags) !Dir { |
| 68 | std.debug.assert(sub_path.len <= sys.NAME_MAX); |
| 69 | var buf: [sys.NAME_MAX + 1]u8 = undefined; |
| 70 | @memcpy(buf[0..sub_path.len], sub_path); |
| 71 | buf[sub_path.len] = 0; |
| 72 | return openDir(self, buf[0..sub_path.len :0], flags); |
| 73 | } |
| 74 | |
| 75 | pub const OpenDirFlags = struct { |
| 76 | // |
| 77 | }; |
| 78 | |
| 79 | /// temporary method for interacting with other std apis we don't have our own version of |
| 80 | pub fn to_std(self: Dir) std.Io.Dir { |
| 81 | return .{ .handle = @intFromEnum(self.fd) }; |
| 82 | } |
| 83 | |
| 84 | pub fn readFileAlloc(self: Dir, allocator: std.mem.Allocator, file_path: [:0]const u8, max_bytes: usize) ![:0]u8 { |
| 85 | var file = try self.openFile(file_path, .{}); |
| 86 | defer file.close(); |
| 87 | const stat_size = std.math.cast(usize, try file.getEndPos()) orelse return error.FileTooBig; |
| 88 | return file.readToEndAlloc(allocator, max_bytes, stat_size); |
| 89 | } |
| 90 | |
| 91 | pub fn makeDir(self: Dir, sub_path: [:0]const u8) !void { |
| 92 | try sys.mkdirat(@intFromEnum(self.fd), sub_path, 0o755); |
| 93 | } |
| 94 | |
| 95 | pub fn statFile(self: Dir, sub_path: [:0]const u8) !File.Stat { |
| 96 | return .fromPosix(try sys.fstatat(@intFromEnum(self.fd), sub_path, 0)); |
| 97 | } |
| 98 | pub fn statFileC(self: Dir, sub_path: []const u8) !File.Stat { |
| 99 | std.debug.assert(sub_path.len <= sys.NAME_MAX); |
| 100 | var buf: [sys.NAME_MAX + 1]u8 = undefined; |
| 101 | @memcpy(buf[0..sub_path.len], sub_path); |
| 102 | buf[sub_path.len] = 0; |
| 103 | return statFile(self, buf[0..sub_path.len :0]); |
| 104 | } |
| 105 | |
| 106 | pub fn makePath(self: Dir, sub_path: [:0]const u8) !void { |
| 107 | var it = std.fs.path.componentIterator(sub_path); |
| 108 | var component = it.last() orelse return; |
| 109 | var zuffer: [sys.NAME_MAX + 1]u8 = undefined; |
| 110 | while (true) { |
| 111 | @memcpy(zuffer[0..component.path.len], component.path); |
| 112 | zuffer[component.path.len] = 0; |
| 113 | self.makeDir(zuffer[0..component.path.len :0]) catch |err| switch (err) { |
| 114 | error.EEXIST => { |
| 115 | // stat the file and return an error if it's not a directory |
| 116 | // this is important because otherwise a dangling symlink |
| 117 | // could cause an infinite loop |
| 118 | check_dir: { |
| 119 | // workaround for windows, see https://github.com/ziglang/zig/issues/16738 |
| 120 | const fstat = self.statFile(zuffer[0..component.path.len :0]) catch |stat_err| switch (stat_err) { |
| 121 | error.EISDIR => break :check_dir, |
| 122 | else => |e| return e, |
| 123 | }; |
| 124 | if (fstat.kind() != .DIR) return error.NotDir; |
| 125 | } |
| 126 | }, |
| 127 | error.ENOENT => |e| { |
| 128 | component = it.previous() orelse return e; |
| 129 | continue; |
| 130 | }, |
| 131 | else => |e| return e, |
| 132 | }; |
| 133 | component = it.next() orelse return; |
| 134 | } |
| 135 | } |
| 136 | pub fn makePathC(self: Dir, sub_path: []const u8) !void { |
| 137 | std.debug.assert(sub_path.len <= sys.PATH_MAX); |
| 138 | var buf: [sys.PATH_MAX + 1]u8 = undefined; |
| 139 | @memcpy(buf[0..sub_path.len], sub_path); |
| 140 | buf[sub_path.len] = 0; |
| 141 | return makePath(self, buf[0..sub_path.len :0]); |
| 142 | } |
| 143 | |
| 144 | pub fn makeOpenPath(self: Dir, sub_path: [:0]const u8, flags: OpenDirFlags) !Dir { |
| 145 | return self.openDir(sub_path, flags) catch |err| switch (err) { |
| 146 | error.ENOENT => { |
| 147 | try self.makePath(sub_path); |
| 148 | return self.openDir(sub_path, flags); |
| 149 | }, |
| 150 | else => |e| return e, |
| 151 | }; |
| 152 | } |
| 153 | pub fn makeOpenPathC(self: Dir, sub_path: []const u8, flags: OpenDirFlags) !Dir { |
| 154 | std.debug.assert(sub_path.len <= sys.PATH_MAX); |
| 155 | var buf: [sys.PATH_MAX + 1]u8 = undefined; |
| 156 | @memcpy(buf[0..sub_path.len], sub_path); |
| 157 | buf[sub_path.len] = 0; |
| 158 | return makeOpenPath(self, buf[0..sub_path.len :0], flags); |
| 159 | } |
| 160 | |
| 161 | pub fn readlink(self: Dir, noalias sub_path: [:0]const u8, noalias buf: []u8) ![:0]u8 { |
| 162 | return sys.readlinkat(@intFromEnum(self.fd), sub_path, buf); |
| 163 | } |
| 164 | |
| 165 | pub fn iterate(self: Dir) Iterator { |
| 166 | return .{ |
| 167 | .dir = self, |
| 168 | .buf = undefined, |
| 169 | .idx = 0, |
| 170 | .len = 0, |
| 171 | .seek = 0, |
| 172 | }; |
| 173 | } |
| 174 | |
| 175 | pub const Iterator = struct { |
| 176 | dir: Dir, |
| 177 | buf: [1024]u8, |
| 178 | idx: usize, |
| 179 | len: usize, |
| 180 | seek: c_long, |
| 181 | |
| 182 | pub fn next(iter: *Iterator) !?Entry { |
| 183 | if (os == .macos) { |
| 184 | if (iter.idx == iter.len) { |
| 185 | const len = try sys.getdirentries(@intFromEnum(iter.dir.fd), &iter.buf, &iter.seek); |
| 186 | if (len == 0) return null; |
| 187 | iter.idx = 0; |
| 188 | iter.len = len; |
| 189 | } |
| 190 | const ent: *align(1) sys.struct_dirent = @ptrCast(&iter.buf[iter.idx]); |
| 191 | iter.idx += ent.reclen; |
| 192 | ent.name[ent.namlen] = 0; |
| 193 | const name = ent.name[0..ent.namlen :0]; |
| 194 | if (std.mem.eql(u8, name, ".")) return next(iter); |
| 195 | if (std.mem.eql(u8, name, "..")) return next(iter); |
| 196 | if (ent.ino == 0) return next(iter); |
| 197 | return .{ |
| 198 | .name = name, |
| 199 | .type = ent.type, |
| 200 | }; |
| 201 | } |
| 202 | if (iter.idx == iter.len) { |
| 203 | const len = try sys.getdents(@intFromEnum(iter.dir.fd), &iter.buf); |
| 204 | if (len == 0) return null; |
| 205 | iter.idx = 0; |
| 206 | iter.len = len; |
| 207 | } |
| 208 | const ent: *align(1) sys.struct_dirent = @ptrCast(&iter.buf[iter.idx]); |
| 209 | iter.idx += ent.reclen; |
| 210 | const name_nidx = std.mem.indexOfScalar(u8, &ent.name, 0).?; |
| 211 | const name = ent.name[0..name_nidx :0]; |
| 212 | if (std.mem.eql(u8, name, ".")) return next(iter); |
| 213 | if (std.mem.eql(u8, name, "..")) return next(iter); |
| 214 | return .{ |
| 215 | .name = name, |
| 216 | .type = ent.type, |
| 217 | }; |
| 218 | } |
| 219 | |
| 220 | pub const Entry = struct { |
| 221 | name: [:0]const u8, |
| 222 | type: sys.DT, |
| 223 | |
| 224 | pub const Type = sys.DT; |
| 225 | }; |
| 226 | }; |
| 227 | |
| 228 | pub fn walk(self: Dir, allocator: std.mem.Allocator) !Walker { |
| 229 | var stack: std.ArrayListUnmanaged(Walker.StackItem) = .empty; |
| 230 | |
| 231 | try stack.append(allocator, .{ |
| 232 | .iter = self.iterate(), |
| 233 | .dirname_len = 0, |
| 234 | }); |
| 235 | |
| 236 | return .{ |
| 237 | .allocator = allocator, |
| 238 | .stack = stack, |
| 239 | .name_buffer = .empty, |
| 240 | }; |
| 241 | } |
| 242 | |
| 243 | pub const Walker = struct { |
| 244 | allocator: std.mem.Allocator, |
| 245 | stack: std.ArrayListUnmanaged(StackItem), |
| 246 | name_buffer: std.ArrayListUnmanaged(u8), |
| 247 | |
| 248 | pub const Entry = struct { |
| 249 | dir: Dir, |
| 250 | basename: [:0]const u8, |
| 251 | path: [:0]const u8, |
| 252 | type: sys.DT, |
| 253 | }; |
| 254 | |
| 255 | const StackItem = struct { |
| 256 | iter: Dir.Iterator, |
| 257 | dirname_len: usize, |
| 258 | }; |
| 259 | |
| 260 | pub fn next(self: *Walker) !?Walker.Entry { |
| 261 | const gpa = self.allocator; |
| 262 | while (self.stack.items.len != 0) { |
| 263 | var top = &self.stack.items[self.stack.items.len - 1]; |
| 264 | var containing = top; |
| 265 | var dirname_len = top.dirname_len; |
| 266 | if (top.iter.next() catch |err| { |
| 267 | var item = self.stack.pop().?; |
| 268 | if (self.stack.items.len != 0) item.iter.dir.close(); |
| 269 | return err; |
| 270 | }) |base| { |
| 271 | self.name_buffer.shrinkRetainingCapacity(dirname_len); |
| 272 | if (self.name_buffer.items.len != 0) { |
| 273 | try self.name_buffer.append(gpa, std.fs.path.sep); |
| 274 | dirname_len += 1; |
| 275 | } |
| 276 | try self.name_buffer.ensureUnusedCapacity(gpa, base.name.len + 1); |
| 277 | self.name_buffer.appendSliceAssumeCapacity(base.name); |
| 278 | self.name_buffer.appendAssumeCapacity(0); |
| 279 | if (base.type == .DIR) { |
| 280 | var new_dir = top.iter.dir.openDir(base.name, .{}) catch |err| switch (err) { |
| 281 | error.ENAMETOOLONG => unreachable, // no path sep in base.name |
| 282 | else => |e| return e, |
| 283 | }; |
| 284 | { |
| 285 | errdefer new_dir.close(); |
| 286 | try self.stack.append(gpa, .{ |
| 287 | .iter = new_dir.iterate(), // was iterateAssumeFirstIteration |
| 288 | .dirname_len = self.name_buffer.items.len - 1, |
| 289 | }); |
| 290 | top = &self.stack.items[self.stack.items.len - 1]; |
| 291 | containing = &self.stack.items[self.stack.items.len - 2]; |
| 292 | } |
| 293 | } |
| 294 | return .{ |
| 295 | .dir = containing.iter.dir, |
| 296 | .basename = self.name_buffer.items[dirname_len .. self.name_buffer.items.len - 1 :0], |
| 297 | .path = self.name_buffer.items[0 .. self.name_buffer.items.len - 1 :0], |
| 298 | .type = base.type, |
| 299 | }; |
| 300 | } else { |
| 301 | var item = self.stack.pop().?; |
| 302 | if (self.stack.items.len != 0) item.iter.dir.close(); |
| 303 | } |
| 304 | } |
| 305 | return null; |
| 306 | } |
| 307 | |
| 308 | pub fn deinit(self: *Walker) void { |
| 309 | const gpa = self.allocator; |
| 310 | // Close any remaining directories except the initial one (which is always at index 0) |
| 311 | if (self.stack.items.len > 1) { |
| 312 | for (self.stack.items[1..]) |*item| { |
| 313 | item.iter.dir.close(); |
| 314 | } |
| 315 | } |
| 316 | self.stack.deinit(gpa); |
| 317 | self.name_buffer.deinit(gpa); |
| 318 | } |
| 319 | }; |
| 320 | |
| 321 | pub fn rename(self: Dir, old: [:0]const u8, new: [:0]const u8) !void { |
| 322 | return sys.renameat(@intFromEnum(self.fd), old.ptr, @intFromEnum(self.fd), new.ptr); |
| 323 | } |
| 324 | pub fn renameC(self: Dir, old: []const u8, new: []const u8) !void { |
| 325 | std.debug.assert(old.len <= sys.NAME_MAX); |
| 326 | std.debug.assert(new.len <= sys.NAME_MAX); |
| 327 | var old_buf: [sys.NAME_MAX + 1]u8 = undefined; |
| 328 | var new_buf: [sys.NAME_MAX + 1]u8 = undefined; |
| 329 | @memcpy(old_buf[0..old.len], old); |
| 330 | @memcpy(new_buf[0..new.len], new); |
| 331 | old_buf[old.len] = 0; |
| 332 | new_buf[new.len] = 0; |
| 333 | return rename(self, old_buf[0..old.len :0], new_buf[0..new.len :0]); |
| 334 | } |
| 335 | |
| 336 | pub fn createFile(self: Dir, sub_path: [:0]const u8, flags: CreateFlags) !File { |
| 337 | var oflag: c_int = 0; |
| 338 | oflag |= if (flags.read) sys.O.RDWR else sys.O.WRONLY; |
| 339 | oflag |= sys.O.CREAT; |
| 340 | if (flags.truncate) oflag |= sys.O.TRUNC; |
| 341 | if (flags.exclusive) oflag |= sys.O.EXCL; |
| 342 | oflag |= sys.O.CLOEXEC; |
| 343 | return .{ .fd = @enumFromInt(try sys.openat4(@intFromEnum(self.fd), sub_path.ptr, oflag, flags.mode)) }; |
| 344 | } |
| 345 | pub fn createFileC(self: Dir, sub_path: []const u8, flags: CreateFlags) !File { |
| 346 | std.debug.assert(sub_path.len <= sys.PATH_MAX); |
| 347 | var buf: [sys.PATH_MAX + 1]u8 = undefined; |
| 348 | @memcpy(buf[0..sub_path.len], sub_path); |
| 349 | buf[sub_path.len] = 0; |
| 350 | return createFile(self, buf[0..sub_path.len :0], flags); |
| 351 | } |
| 352 | |
| 353 | pub const CreateFlags = packed struct { |
| 354 | /// Whether the file will be created with read access. |
| 355 | read: bool = false, |
| 356 | /// If the file already exists, and is a regular file, and the access mode allows writing, it will be truncated to length 0. |
| 357 | truncate: bool = true, |
| 358 | /// Ensures that this open call creates the file, otherwise causes `error.EEXIST` to be returned. |
| 359 | exclusive: bool = false, |
| 360 | /// The file system mode the file will be created with. |
| 361 | mode: File.Mode = 0o666, |
| 362 | }; |
| 363 | |
| 364 | pub fn access(self: Dir, sub_path: [:0]const u8, amode: AccessMode, flag: c_int) !void { |
| 365 | return sys.faccessat(@intFromEnum(self.fd), sub_path, @bitCast(amode), flag); |
| 366 | } |
| 367 | |
| 368 | pub const AccessMode = packed struct(c_uint) { |
| 369 | readable: bool = false, |
| 370 | writable: bool = false, |
| 371 | executable: bool = false, |
| 372 | _: u29 = 0, |
| 373 | }; |
| 374 | |
| 375 | pub fn realpath(self: Dir, sub_path: [:0]const u8, buf: *[sys.PATH_MAX]u8) ![:0]u8 { |
| 376 | if (std.mem.eql(u8, sub_path, ".")) { |
| 377 | if (@intFromEnum(self.fd) == sys.AT.FDCWD) { |
| 378 | return nfs.cwdpath(buf); |
| 379 | } |
| 380 | return nfs.realdpath(self.fd, buf); |
| 381 | } |
| 382 | var file = try self.openFile(sub_path, .{}); |
| 383 | defer file.close(); |
| 384 | return nfs.realdpath(file.fd, buf); |
| 385 | } |
| 386 | |
| 387 | pub fn realpathAlloc(self: Dir, allocator: std.mem.Allocator, sub_path: [:0]const u8) ![:0]u8 { |
| 388 | var buf: [sys.PATH_MAX]u8 = undefined; |
| 389 | const actual = try self.realpath(sub_path, &buf); |
| 390 | return allocator.dupeZ(u8, actual); |
| 391 | } |
| 392 | |
| 393 | pub fn deleteFile(self: Dir, sub_path: [:0]const u8) !void { |
| 394 | return sys.unlinkat(@intFromEnum(self.fd), sub_path.ptr, 0); |
| 395 | } |
| 396 | |
| 397 | pub fn deleteDir(self: Dir, sub_path: [:0]const u8) !void { |
| 398 | return sys.unlinkat(@intFromEnum(self.fd), sub_path.ptr, sys.AT.REMOVEDIR); |
| 399 | } |
| 400 | |
| 401 | pub fn deleteTree(self: Dir, sub_path: [:0]const u8) !void { |
| 402 | var dir = try self.openDir(sub_path, .{}); |
| 403 | { |
| 404 | errdefer dir.close(); |
| 405 | var iter = dir.iterate(); |
| 406 | while (try iter.next()) |entry| { |
| 407 | switch (entry.type) { |
| 408 | .DIR => { |
| 409 | try dir.deleteTree(entry.name); |
| 410 | }, |
| 411 | else => { |
| 412 | try dir.deleteFile(entry.name); |
| 413 | }, |
| 414 | } |
| 415 | } |
| 416 | dir.close(); |
| 417 | } |
| 418 | try self.deleteDir(sub_path); |
| 419 | } |
| 420 | |
| 421 | pub fn exists(self: Dir, sub_path: [:0]const u8) !bool { |
| 422 | self.access(sub_path, .{}, 0) catch |err| switch (err) { |
| 423 | error.ENOENT => return false, |
| 424 | else => |e| return e, |
| 425 | }; |
| 426 | return true; |
| 427 | } |
| 428 | |
| 429 | pub fn existsDir(self: Dir, sub_path: [:0]const u8) !bool { |
| 430 | var dir = self.openDir(sub_path, .{}) catch |err| switch (err) { |
| 431 | error.ENOENT => return false, |
| 432 | error.ENOTDIR => return false, |
| 433 | else => |e| return e, |
| 434 | }; |
| 435 | dir.close(); |
| 436 | return true; |
| 437 | } |
| 438 | |
| 439 | pub fn copyFile(source_dir: Dir, source_path: [:0]const u8, dest_dir: Dir, dest_path: [:0]const u8) !void { |
| 440 | const source_file = try source_dir.openFile(source_path, .{}); |
| 441 | defer source_file.close(); |
| 442 | const source_stat = try source_file.stat(); |
| 443 | |
| 444 | const dest_file = try dest_dir.createFile(dest_path, .{ .exclusive = true, .mode = source_stat.mode }); |
| 445 | defer dest_file.close(); |
| 446 | |
| 447 | if (os == .linux) { |
| 448 | var off: u64 = 0; |
| 449 | var off_in: sys.off_t = 0; |
| 450 | var off_out: sys.off_t = 0; |
| 451 | while (true) { |
| 452 | const amt = try sys.copy_file_range(@intFromEnum(source_file.fd), &off_in, @intFromEnum(dest_file.fd), &off_out, source_stat.size - off, 0); |
| 453 | off += amt; |
| 454 | if (amt == 0) break; |
| 455 | } |
| 456 | return; |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | pub fn dup(self: Dir) !Dir { |
| 461 | return .{ .fd = @enumFromInt(try sys.dup(@intFromEnum(self.fd))) }; |
| 462 | } |