1const std = @import("std");
2const builtin = @import("builtin");
3
4const nfs = @import("./nfs.zig");
5const File = nfs.File;
6const Dir = @This();
7
8const os = builtin.target.os.tag;
9
10const 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
19pub const cwd = nfs.cwd;
20
21fd: nfs.Handle,
22
23// Resource allocation may fail; resource deallocation must succeed.
24pub fn close(self: Dir) void {
25 return sys.close(@intFromEnum(self.fd)) catch if (builtin.mode == .Debug) unreachable;
26}
27
28pub 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}
49pub 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
57pub 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
62pub 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}
67pub 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
75pub const OpenDirFlags = struct {
76 //
77};
78
79/// temporary method for interacting with other std apis we don't have our own version of
80pub fn to_std(self: Dir) std.Io.Dir {
81 return .{ .handle = @intFromEnum(self.fd) };
82}
83
84pub 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
91pub fn makeDir(self: Dir, sub_path: [:0]const u8) !void {
92 try sys.mkdirat(@intFromEnum(self.fd), sub_path, 0o755);
93}
94
95pub fn statFile(self: Dir, sub_path: [:0]const u8) !File.Stat {
96 return .fromPosix(try sys.fstatat(@intFromEnum(self.fd), sub_path, 0));
97}
98pub 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
106pub 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}
136pub 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
144pub 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}
153pub 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
161pub 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
165pub 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
175pub 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
228pub 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
243pub 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
321pub 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}
324pub 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
336pub 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}
345pub 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
353pub 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
364pub 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
368pub const AccessMode = packed struct(c_uint) {
369 readable: bool = false,
370 writable: bool = false,
371 executable: bool = false,
372 _: u29 = 0,
373};
374
375pub 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
387pub 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
393pub fn deleteFile(self: Dir, sub_path: [:0]const u8) !void {
394 return sys.unlinkat(@intFromEnum(self.fd), sub_path.ptr, 0);
395}
396
397pub 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
401pub 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
421pub 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
429pub 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
439pub 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
460pub fn dup(self: Dir) !Dir {
461 return .{ .fd = @enumFromInt(try sys.dup(@intFromEnum(self.fd))) };
462}