master
1const File = @This();
2
3const builtin = @import("builtin");
4const native_os = builtin.os.tag;
5const is_windows = native_os == .windows;
6
7const std = @import("../std.zig");
8const Io = std.Io;
9const Os = std.builtin.Os;
10const Allocator = std.mem.Allocator;
11const posix = std.posix;
12const math = std.math;
13const assert = std.debug.assert;
14const linux = std.os.linux;
15const windows = std.os.windows;
16const maxInt = std.math.maxInt;
17const Alignment = std.mem.Alignment;
18
19/// The OS-specific file descriptor or file handle.
20handle: Handle,
21
22pub const Handle = Io.File.Handle;
23pub const Mode = Io.File.Mode;
24pub const INode = Io.File.INode;
25pub const Uid = posix.uid_t;
26pub const Gid = posix.gid_t;
27pub const Kind = Io.File.Kind;
28
29/// This is the default mode given to POSIX operating systems for creating
30/// files. `0o666` is "-rw-rw-rw-" which is counter-intuitive at first,
31/// since most people would expect "-rw-r--r--", for example, when using
32/// the `touch` command, which would correspond to `0o644`. However, POSIX
33/// libc implementations use `0o666` inside `fopen` and then rely on the
34/// process-scoped "umask" setting to adjust this number for file creation.
35pub const default_mode: Mode = if (Mode == u0) 0 else 0o666;
36
37/// Deprecated in favor of `Io.File.OpenError`.
38pub const OpenError = Io.File.OpenError || error{WouldBlock};
39/// Deprecated in favor of `Io.File.OpenMode`.
40pub const OpenMode = Io.File.OpenMode;
41/// Deprecated in favor of `Io.File.Lock`.
42pub const Lock = Io.File.Lock;
43/// Deprecated in favor of `Io.File.OpenFlags`.
44pub const OpenFlags = Io.File.OpenFlags;
45
46pub const CreateFlags = struct {
47 /// Whether the file will be created with read access.
48 read: bool = false,
49
50 /// If the file already exists, and is a regular file, and the access
51 /// mode allows writing, it will be truncated to length 0.
52 truncate: bool = true,
53
54 /// Ensures that this open call creates the file, otherwise causes
55 /// `error.PathAlreadyExists` to be returned.
56 exclusive: bool = false,
57
58 /// Open the file with an advisory lock to coordinate with other processes
59 /// accessing it at the same time. An exclusive lock will prevent other
60 /// processes from acquiring a lock. A shared lock will prevent other
61 /// processes from acquiring a exclusive lock, but does not prevent
62 /// other process from getting their own shared locks.
63 ///
64 /// The lock is advisory, except on Linux in very specific circumstances[1].
65 /// This means that a process that does not respect the locking API can still get access
66 /// to the file, despite the lock.
67 ///
68 /// On these operating systems, the lock is acquired atomically with
69 /// opening the file:
70 /// * Darwin
71 /// * DragonFlyBSD
72 /// * FreeBSD
73 /// * Haiku
74 /// * NetBSD
75 /// * OpenBSD
76 /// On these operating systems, the lock is acquired via a separate syscall
77 /// after opening the file:
78 /// * Linux
79 /// * Windows
80 ///
81 /// [1]: https://www.kernel.org/doc/Documentation/filesystems/mandatory-locking.txt
82 lock: Lock = .none,
83
84 /// Sets whether or not to wait until the file is locked to return. If set to true,
85 /// `error.WouldBlock` will be returned. Otherwise, the file will wait until the file
86 /// is available to proceed.
87 lock_nonblocking: bool = false,
88
89 /// For POSIX systems this is the file system mode the file will
90 /// be created with. On other systems this is always 0.
91 mode: Mode = default_mode,
92};
93
94pub fn stdout() File {
95 return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdOutput else posix.STDOUT_FILENO };
96}
97
98pub fn stderr() File {
99 return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdError else posix.STDERR_FILENO };
100}
101
102pub fn stdin() File {
103 return .{ .handle = if (is_windows) windows.peb().ProcessParameters.hStdInput else posix.STDIN_FILENO };
104}
105
106/// Upon success, the stream is in an uninitialized state. To continue using it,
107/// you must use the open() function.
108pub fn close(self: File) void {
109 if (is_windows) {
110 windows.CloseHandle(self.handle);
111 } else {
112 posix.close(self.handle);
113 }
114}
115
116pub const SyncError = posix.SyncError;
117
118/// Blocks until all pending file contents and metadata modifications
119/// for the file have been synchronized with the underlying filesystem.
120///
121/// Note that this does not ensure that metadata for the
122/// directory containing the file has also reached disk.
123pub fn sync(self: File) SyncError!void {
124 return posix.fsync(self.handle);
125}
126
127/// Test whether the file refers to a terminal.
128/// See also `getOrEnableAnsiEscapeSupport` and `supportsAnsiEscapeCodes`.
129pub fn isTty(self: File) bool {
130 return posix.isatty(self.handle);
131}
132
133pub fn isCygwinPty(file: File) bool {
134 if (builtin.os.tag != .windows) return false;
135
136 const handle = file.handle;
137
138 // If this is a MSYS2/cygwin pty, then it will be a named pipe with a name in one of these formats:
139 // msys-[...]-ptyN-[...]
140 // cygwin-[...]-ptyN-[...]
141 //
142 // Example: msys-1888ae32e00d56aa-pty0-to-master
143
144 // First, just check that the handle is a named pipe.
145 // This allows us to avoid the more costly NtQueryInformationFile call
146 // for handles that aren't named pipes.
147 {
148 var io_status: windows.IO_STATUS_BLOCK = undefined;
149 var device_info: windows.FILE_FS_DEVICE_INFORMATION = undefined;
150 const rc = windows.ntdll.NtQueryVolumeInformationFile(handle, &io_status, &device_info, @sizeOf(windows.FILE_FS_DEVICE_INFORMATION), .FileFsDeviceInformation);
151 switch (rc) {
152 .SUCCESS => {},
153 else => return false,
154 }
155 if (device_info.DeviceType != windows.FILE_DEVICE_NAMED_PIPE) return false;
156 }
157
158 const name_bytes_offset = @offsetOf(windows.FILE_NAME_INFO, "FileName");
159 // `NAME_MAX` UTF-16 code units (2 bytes each)
160 // This buffer may not be long enough to handle *all* possible paths
161 // (PATH_MAX_WIDE would be necessary for that), but because we only care
162 // about certain paths and we know they must be within a reasonable length,
163 // we can use this smaller buffer and just return false on any error from
164 // NtQueryInformationFile.
165 const num_name_bytes = windows.MAX_PATH * 2;
166 var name_info_bytes align(@alignOf(windows.FILE_NAME_INFO)) = [_]u8{0} ** (name_bytes_offset + num_name_bytes);
167
168 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
169 const rc = windows.ntdll.NtQueryInformationFile(handle, &io_status_block, &name_info_bytes, @intCast(name_info_bytes.len), .FileNameInformation);
170 switch (rc) {
171 .SUCCESS => {},
172 .INVALID_PARAMETER => unreachable,
173 else => return false,
174 }
175
176 const name_info: *const windows.FILE_NAME_INFO = @ptrCast(&name_info_bytes);
177 const name_bytes = name_info_bytes[name_bytes_offset .. name_bytes_offset + name_info.FileNameLength];
178 const name_wide = std.mem.bytesAsSlice(u16, name_bytes);
179 // The name we get from NtQueryInformationFile will be prefixed with a '\', e.g. \msys-1888ae32e00d56aa-pty0-to-master
180 return (std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'm', 's', 'y', 's', '-' }) or
181 std.mem.startsWith(u16, name_wide, &[_]u16{ '\\', 'c', 'y', 'g', 'w', 'i', 'n', '-' })) and
182 std.mem.indexOf(u16, name_wide, &[_]u16{ '-', 'p', 't', 'y' }) != null;
183}
184
185/// Returns whether or not ANSI escape codes will be treated as such,
186/// and attempts to enable support for ANSI escape codes if necessary
187/// (on Windows).
188///
189/// Returns `true` if ANSI escape codes are supported or support was
190/// successfully enabled. Returns false if ANSI escape codes are not
191/// supported or support was unable to be enabled.
192///
193/// See also `supportsAnsiEscapeCodes`.
194pub fn getOrEnableAnsiEscapeSupport(self: File) bool {
195 if (builtin.os.tag == .windows) {
196 var original_console_mode: windows.DWORD = 0;
197
198 // For Windows Terminal, VT Sequences processing is enabled by default.
199 if (windows.kernel32.GetConsoleMode(self.handle, &original_console_mode) != 0) {
200 if (original_console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
201
202 // For Windows Console, VT Sequences processing support was added in Windows 10 build 14361, but disabled by default.
203 // https://devblogs.microsoft.com/commandline/tmux-support-arrives-for-bash-on-ubuntu-on-windows/
204 //
205 // Note: In Microsoft's example for enabling virtual terminal processing, it
206 // shows attempting to enable `DISABLE_NEWLINE_AUTO_RETURN` as well:
207 // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example-of-enabling-virtual-terminal-processing
208 // This is avoided because in the old Windows Console, that flag causes \n (as opposed to \r\n)
209 // to behave unexpectedly (the cursor moves down 1 row but remains on the same column).
210 // Additionally, the default console mode in Windows Terminal does not have
211 // `DISABLE_NEWLINE_AUTO_RETURN` set, so by only enabling `ENABLE_VIRTUAL_TERMINAL_PROCESSING`
212 // we end up matching the mode of Windows Terminal.
213 const requested_console_modes = windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
214 const console_mode = original_console_mode | requested_console_modes;
215 if (windows.kernel32.SetConsoleMode(self.handle, console_mode) != 0) return true;
216 }
217
218 return self.isCygwinPty();
219 }
220 return self.supportsAnsiEscapeCodes();
221}
222
223/// Test whether ANSI escape codes will be treated as such without
224/// attempting to enable support for ANSI escape codes.
225///
226/// See also `getOrEnableAnsiEscapeSupport`.
227pub fn supportsAnsiEscapeCodes(self: File) bool {
228 if (builtin.os.tag == .windows) {
229 var console_mode: windows.DWORD = 0;
230 if (windows.kernel32.GetConsoleMode(self.handle, &console_mode) != 0) {
231 if (console_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0) return true;
232 }
233
234 return self.isCygwinPty();
235 }
236 if (builtin.os.tag == .wasi) {
237 // WASI sanitizes stdout when fd is a tty so ANSI escape codes
238 // will not be interpreted as actual cursor commands, and
239 // stderr is always sanitized.
240 return false;
241 }
242 if (self.isTty()) {
243 if (self.handle == posix.STDOUT_FILENO or self.handle == posix.STDERR_FILENO) {
244 if (posix.getenvZ("TERM")) |term| {
245 if (std.mem.eql(u8, term, "dumb"))
246 return false;
247 }
248 }
249 return true;
250 }
251 return false;
252}
253
254pub const SetEndPosError = posix.TruncateError;
255
256/// Shrinks or expands the file.
257/// The file offset after this call is left unchanged.
258pub fn setEndPos(self: File, length: u64) SetEndPosError!void {
259 try posix.ftruncate(self.handle, length);
260}
261
262pub const SeekError = posix.SeekError;
263
264/// Repositions read/write file offset relative to the current offset.
265/// TODO: integrate with async I/O
266pub fn seekBy(self: File, offset: i64) SeekError!void {
267 return posix.lseek_CUR(self.handle, offset);
268}
269
270/// Repositions read/write file offset relative to the end.
271/// TODO: integrate with async I/O
272pub fn seekFromEnd(self: File, offset: i64) SeekError!void {
273 return posix.lseek_END(self.handle, offset);
274}
275
276/// Repositions read/write file offset relative to the beginning.
277/// TODO: integrate with async I/O
278pub fn seekTo(self: File, offset: u64) SeekError!void {
279 return posix.lseek_SET(self.handle, offset);
280}
281
282pub const GetSeekPosError = posix.SeekError || StatError;
283
284/// TODO: integrate with async I/O
285pub fn getPos(self: File) GetSeekPosError!u64 {
286 return posix.lseek_CUR_get(self.handle);
287}
288
289pub const GetEndPosError = std.os.windows.GetFileSizeError || StatError;
290
291/// TODO: integrate with async I/O
292pub fn getEndPos(self: File) GetEndPosError!u64 {
293 if (builtin.os.tag == .windows) {
294 return windows.GetFileSizeEx(self.handle);
295 }
296 return (try self.stat()).size;
297}
298
299pub const ModeError = StatError;
300
301/// TODO: integrate with async I/O
302pub fn mode(self: File) ModeError!Mode {
303 if (builtin.os.tag == .windows) {
304 return 0;
305 }
306 return (try self.stat()).mode;
307}
308
309pub const Stat = Io.File.Stat;
310
311pub const StatError = posix.FStatError;
312
313/// Returns `Stat` containing basic information about the `File`.
314pub fn stat(self: File) StatError!Stat {
315 var threaded: Io.Threaded = .init_single_threaded;
316 const io = threaded.ioBasic();
317 return Io.File.stat(.{ .handle = self.handle }, io);
318}
319
320pub const ChmodError = posix.FChmodError;
321
322/// Changes the mode of the file.
323/// The process must have the correct privileges in order to do this
324/// successfully, or must have the effective user ID matching the owner
325/// of the file.
326pub fn chmod(self: File, new_mode: Mode) ChmodError!void {
327 try posix.fchmod(self.handle, new_mode);
328}
329
330pub const ChownError = posix.FChownError;
331
332/// Changes the owner and group of the file.
333/// The process must have the correct privileges in order to do this
334/// successfully. The group may be changed by the owner of the file to
335/// any group of which the owner is a member. If the owner or group is
336/// specified as `null`, the ID is not changed.
337pub fn chown(self: File, owner: ?Uid, group: ?Gid) ChownError!void {
338 try posix.fchown(self.handle, owner, group);
339}
340
341/// Cross-platform representation of permissions on a file.
342/// The `readonly` and `setReadonly` are the only methods available across all platforms.
343/// Platform-specific functionality is available through the `inner` field.
344pub const Permissions = struct {
345 /// You may use the `inner` field to use platform-specific functionality
346 inner: switch (builtin.os.tag) {
347 .windows => PermissionsWindows,
348 else => PermissionsUnix,
349 },
350
351 const Self = @This();
352
353 /// Returns `true` if permissions represent an unwritable file.
354 /// On Unix, `true` is returned only if no class has write permissions.
355 pub fn readOnly(self: Self) bool {
356 return self.inner.readOnly();
357 }
358
359 /// Sets whether write permissions are provided.
360 /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`.
361 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
362 pub fn setReadOnly(self: *Self, read_only: bool) void {
363 self.inner.setReadOnly(read_only);
364 }
365};
366
367pub const PermissionsWindows = struct {
368 attributes: windows.DWORD,
369
370 const Self = @This();
371
372 /// Returns `true` if permissions represent an unwritable file.
373 pub fn readOnly(self: Self) bool {
374 return self.attributes & windows.FILE_ATTRIBUTE_READONLY != 0;
375 }
376
377 /// Sets whether write permissions are provided.
378 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
379 pub fn setReadOnly(self: *Self, read_only: bool) void {
380 if (read_only) {
381 self.attributes |= windows.FILE_ATTRIBUTE_READONLY;
382 } else {
383 self.attributes &= ~@as(windows.DWORD, windows.FILE_ATTRIBUTE_READONLY);
384 }
385 }
386};
387
388pub const PermissionsUnix = struct {
389 mode: Mode,
390
391 const Self = @This();
392
393 /// Returns `true` if permissions represent an unwritable file.
394 /// `true` is returned only if no class has write permissions.
395 pub fn readOnly(self: Self) bool {
396 return self.mode & 0o222 == 0;
397 }
398
399 /// Sets whether write permissions are provided.
400 /// This affects *all* classes. If this is undesired, use `unixSet`.
401 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
402 pub fn setReadOnly(self: *Self, read_only: bool) void {
403 if (read_only) {
404 self.mode &= ~@as(Mode, 0o222);
405 } else {
406 self.mode |= @as(Mode, 0o222);
407 }
408 }
409
410 pub const Class = enum(u2) {
411 user = 2,
412 group = 1,
413 other = 0,
414 };
415
416 pub const Permission = enum(u3) {
417 read = 0o4,
418 write = 0o2,
419 execute = 0o1,
420 };
421
422 /// Returns `true` if the chosen class has the selected permission.
423 /// This method is only available on Unix platforms.
424 pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
425 const mask = @as(Mode, @intFromEnum(permission)) << @as(u3, @intFromEnum(class)) * 3;
426 return self.mode & mask != 0;
427 }
428
429 /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
430 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
431 pub fn unixSet(self: *Self, class: Class, permissions: struct {
432 read: ?bool = null,
433 write: ?bool = null,
434 execute: ?bool = null,
435 }) void {
436 const shift = @as(u3, @intFromEnum(class)) * 3;
437 if (permissions.read) |r| {
438 if (r) {
439 self.mode |= @as(Mode, 0o4) << shift;
440 } else {
441 self.mode &= ~(@as(Mode, 0o4) << shift);
442 }
443 }
444 if (permissions.write) |w| {
445 if (w) {
446 self.mode |= @as(Mode, 0o2) << shift;
447 } else {
448 self.mode &= ~(@as(Mode, 0o2) << shift);
449 }
450 }
451 if (permissions.execute) |x| {
452 if (x) {
453 self.mode |= @as(Mode, 0o1) << shift;
454 } else {
455 self.mode &= ~(@as(Mode, 0o1) << shift);
456 }
457 }
458 }
459
460 /// Returns a `Permissions` struct representing the permissions from the passed mode.
461 pub fn unixNew(new_mode: Mode) Self {
462 return Self{
463 .mode = new_mode,
464 };
465 }
466};
467
468pub const SetPermissionsError = ChmodError;
469
470/// Sets permissions according to the provided `Permissions` struct.
471/// This method is *NOT* available on WASI
472pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
473 switch (builtin.os.tag) {
474 .windows => {
475 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
476 var info = windows.FILE_BASIC_INFORMATION{
477 .CreationTime = 0,
478 .LastAccessTime = 0,
479 .LastWriteTime = 0,
480 .ChangeTime = 0,
481 .FileAttributes = permissions.inner.attributes,
482 };
483 const rc = windows.ntdll.NtSetInformationFile(
484 self.handle,
485 &io_status_block,
486 &info,
487 @sizeOf(windows.FILE_BASIC_INFORMATION),
488 .FileBasicInformation,
489 );
490 switch (rc) {
491 .SUCCESS => return,
492 .INVALID_HANDLE => unreachable,
493 .ACCESS_DENIED => return error.AccessDenied,
494 else => return windows.unexpectedStatus(rc),
495 }
496 },
497 .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
498 else => {
499 try self.chmod(permissions.inner.mode);
500 },
501 }
502}
503
504pub const UpdateTimesError = posix.FutimensError || windows.SetFileTimeError;
505
506/// The underlying file system may have a different granularity than nanoseconds,
507/// and therefore this function cannot guarantee any precision will be stored.
508/// Further, the maximum value is limited by the system ABI. When a value is provided
509/// that exceeds this range, the value is clamped to the maximum.
510/// TODO: integrate with async I/O
511pub fn updateTimes(
512 self: File,
513 /// access timestamp in nanoseconds
514 atime: Io.Timestamp,
515 /// last modification timestamp in nanoseconds
516 mtime: Io.Timestamp,
517) UpdateTimesError!void {
518 if (builtin.os.tag == .windows) {
519 const atime_ft = windows.nanoSecondsToFileTime(atime);
520 const mtime_ft = windows.nanoSecondsToFileTime(mtime);
521 return windows.SetFileTime(self.handle, null, &atime_ft, &mtime_ft);
522 }
523 const times = [2]posix.timespec{
524 posix.timespec{
525 .sec = math.cast(isize, @divFloor(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
526 .nsec = math.cast(isize, @mod(atime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
527 },
528 posix.timespec{
529 .sec = math.cast(isize, @divFloor(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
530 .nsec = math.cast(isize, @mod(mtime.nanoseconds, std.time.ns_per_s)) orelse maxInt(isize),
531 },
532 };
533 try posix.futimens(self.handle, ×);
534}
535
536pub const ReadError = posix.ReadError;
537pub const PReadError = posix.PReadError;
538
539pub fn read(self: File, buffer: []u8) ReadError!usize {
540 if (is_windows) {
541 return windows.ReadFile(self.handle, buffer, null);
542 }
543
544 return posix.read(self.handle, buffer);
545}
546
547/// On Windows, this function currently does alter the file pointer.
548/// https://github.com/ziglang/zig/issues/12783
549pub fn pread(self: File, buffer: []u8, offset: u64) PReadError!usize {
550 if (is_windows) {
551 return windows.ReadFile(self.handle, buffer, offset);
552 }
553
554 return posix.pread(self.handle, buffer, offset);
555}
556
557/// Deprecated in favor of `Reader`.
558pub fn preadAll(self: File, buffer: []u8, offset: u64) PReadError!usize {
559 var index: usize = 0;
560 while (index != buffer.len) {
561 const amt = try self.pread(buffer[index..], offset + index);
562 if (amt == 0) break;
563 index += amt;
564 }
565 return index;
566}
567
568/// See https://github.com/ziglang/zig/issues/7699
569pub fn readv(self: File, iovecs: []const posix.iovec) ReadError!usize {
570 if (is_windows) {
571 if (iovecs.len == 0) return 0;
572 const first = iovecs[0];
573 return windows.ReadFile(self.handle, first.base[0..first.len], null);
574 }
575
576 return posix.readv(self.handle, iovecs);
577}
578
579/// See https://github.com/ziglang/zig/issues/7699
580/// On Windows, this function currently does alter the file pointer.
581/// https://github.com/ziglang/zig/issues/12783
582pub fn preadv(self: File, iovecs: []const posix.iovec, offset: u64) PReadError!usize {
583 if (is_windows) {
584 if (iovecs.len == 0) return 0;
585 const first = iovecs[0];
586 return windows.ReadFile(self.handle, first.base[0..first.len], offset);
587 }
588
589 return posix.preadv(self.handle, iovecs, offset);
590}
591
592pub const WriteError = posix.WriteError;
593pub const PWriteError = posix.PWriteError;
594
595pub fn write(self: File, bytes: []const u8) WriteError!usize {
596 if (is_windows) {
597 return windows.WriteFile(self.handle, bytes, null);
598 }
599
600 return posix.write(self.handle, bytes);
601}
602
603pub fn writeAll(self: File, bytes: []const u8) WriteError!void {
604 var index: usize = 0;
605 while (index < bytes.len) {
606 index += try self.write(bytes[index..]);
607 }
608}
609
610/// Deprecated in favor of `Writer`.
611pub fn pwriteAll(self: File, bytes: []const u8, offset: u64) PWriteError!void {
612 var index: usize = 0;
613 while (index < bytes.len) {
614 index += try self.pwrite(bytes[index..], offset + index);
615 }
616}
617
618/// On Windows, this function currently does alter the file pointer.
619/// https://github.com/ziglang/zig/issues/12783
620pub fn pwrite(self: File, bytes: []const u8, offset: u64) PWriteError!usize {
621 if (is_windows) {
622 return windows.WriteFile(self.handle, bytes, offset);
623 }
624
625 return posix.pwrite(self.handle, bytes, offset);
626}
627
628/// See https://github.com/ziglang/zig/issues/7699
629pub fn writev(self: File, iovecs: []const posix.iovec_const) WriteError!usize {
630 if (is_windows) {
631 // TODO improve this to use WriteFileScatter
632 if (iovecs.len == 0) return 0;
633 const first = iovecs[0];
634 return windows.WriteFile(self.handle, first.base[0..first.len], null);
635 }
636
637 return posix.writev(self.handle, iovecs);
638}
639
640/// See https://github.com/ziglang/zig/issues/7699
641/// On Windows, this function currently does alter the file pointer.
642/// https://github.com/ziglang/zig/issues/12783
643pub fn pwritev(self: File, iovecs: []posix.iovec_const, offset: u64) PWriteError!usize {
644 if (is_windows) {
645 if (iovecs.len == 0) return 0;
646 const first = iovecs[0];
647 return windows.WriteFile(self.handle, first.base[0..first.len], offset);
648 }
649
650 return posix.pwritev(self.handle, iovecs, offset);
651}
652
653/// Deprecated in favor of `Writer`.
654pub const CopyRangeError = posix.CopyFileRangeError;
655
656/// Deprecated in favor of `Writer`.
657pub fn copyRange(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
658 const adjusted_len = math.cast(usize, len) orelse maxInt(usize);
659 const result = try posix.copy_file_range(in.handle, in_offset, out.handle, out_offset, adjusted_len, 0);
660 return result;
661}
662
663/// Deprecated in favor of `Writer`.
664pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u64) CopyRangeError!u64 {
665 var total_bytes_copied: u64 = 0;
666 var in_off = in_offset;
667 var out_off = out_offset;
668 while (total_bytes_copied < len) {
669 const amt_copied = try copyRange(in, in_off, out, out_off, len - total_bytes_copied);
670 if (amt_copied == 0) return total_bytes_copied;
671 total_bytes_copied += amt_copied;
672 in_off += amt_copied;
673 out_off += amt_copied;
674 }
675 return total_bytes_copied;
676}
677
678/// Deprecated in favor of `Io.File.Reader`.
679pub const Reader = Io.File.Reader;
680
681pub const Writer = struct {
682 file: File,
683 err: ?WriteError = null,
684 mode: Writer.Mode = .positional,
685 /// Tracks the true seek position in the file. To obtain the logical
686 /// position, add the buffer size to this value.
687 pos: u64 = 0,
688 sendfile_err: ?SendfileError = null,
689 copy_file_range_err: ?CopyFileRangeError = null,
690 fcopyfile_err: ?FcopyfileError = null,
691 seek_err: ?Writer.SeekError = null,
692 interface: Io.Writer,
693
694 pub const Mode = Reader.Mode;
695
696 pub const SendfileError = error{
697 UnsupportedOperation,
698 SystemResources,
699 InputOutput,
700 BrokenPipe,
701 WouldBlock,
702 Unexpected,
703 };
704
705 pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError;
706
707 pub const FcopyfileError = error{
708 OperationNotSupported,
709 OutOfMemory,
710 Unexpected,
711 };
712
713 pub const SeekError = File.SeekError;
714
715 /// Number of slices to store on the stack, when trying to send as many byte
716 /// vectors through the underlying write calls as possible.
717 const max_buffers_len = 16;
718
719 pub fn init(file: File, buffer: []u8) Writer {
720 return .{
721 .file = file,
722 .interface = initInterface(buffer),
723 .mode = .positional,
724 };
725 }
726
727 /// Positional is more threadsafe, since the global seek position is not
728 /// affected, but when such syscalls are not available, preemptively
729 /// initializing in streaming mode will skip a failed syscall.
730 pub fn initStreaming(file: File, buffer: []u8) Writer {
731 return .{
732 .file = file,
733 .interface = initInterface(buffer),
734 .mode = .streaming,
735 };
736 }
737
738 pub fn initInterface(buffer: []u8) Io.Writer {
739 return .{
740 .vtable = &.{
741 .drain = drain,
742 .sendFile = sendFile,
743 },
744 .buffer = buffer,
745 };
746 }
747
748 /// TODO when this logic moves from fs.File to Io.File the io parameter should be deleted
749 pub fn moveToReader(w: *Writer, io: Io) Reader {
750 defer w.* = undefined;
751 return .{
752 .io = io,
753 .file = .{ .handle = w.file.handle },
754 .mode = w.mode,
755 .pos = w.pos,
756 .interface = Reader.initInterface(w.interface.buffer),
757 .seek_err = w.seek_err,
758 };
759 }
760
761 pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
762 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
763 const handle = w.file.handle;
764 const buffered = io_w.buffered();
765 if (is_windows) switch (w.mode) {
766 .positional, .positional_reading => {
767 if (buffered.len != 0) {
768 const n = windows.WriteFile(handle, buffered, w.pos) catch |err| {
769 w.err = err;
770 return error.WriteFailed;
771 };
772 w.pos += n;
773 return io_w.consume(n);
774 }
775 for (data[0 .. data.len - 1]) |buf| {
776 if (buf.len == 0) continue;
777 const n = windows.WriteFile(handle, buf, w.pos) catch |err| {
778 w.err = err;
779 return error.WriteFailed;
780 };
781 w.pos += n;
782 return io_w.consume(n);
783 }
784 const pattern = data[data.len - 1];
785 if (pattern.len == 0 or splat == 0) return 0;
786 const n = windows.WriteFile(handle, pattern, w.pos) catch |err| {
787 w.err = err;
788 return error.WriteFailed;
789 };
790 w.pos += n;
791 return io_w.consume(n);
792 },
793 .streaming, .streaming_reading => {
794 if (buffered.len != 0) {
795 const n = windows.WriteFile(handle, buffered, null) catch |err| {
796 w.err = err;
797 return error.WriteFailed;
798 };
799 w.pos += n;
800 return io_w.consume(n);
801 }
802 for (data[0 .. data.len - 1]) |buf| {
803 if (buf.len == 0) continue;
804 const n = windows.WriteFile(handle, buf, null) catch |err| {
805 w.err = err;
806 return error.WriteFailed;
807 };
808 w.pos += n;
809 return io_w.consume(n);
810 }
811 const pattern = data[data.len - 1];
812 if (pattern.len == 0 or splat == 0) return 0;
813 const n = windows.WriteFile(handle, pattern, null) catch |err| {
814 w.err = err;
815 return error.WriteFailed;
816 };
817 w.pos += n;
818 return io_w.consume(n);
819 },
820 .failure => return error.WriteFailed,
821 };
822 var iovecs: [max_buffers_len]std.posix.iovec_const = undefined;
823 var len: usize = 0;
824 if (buffered.len > 0) {
825 iovecs[len] = .{ .base = buffered.ptr, .len = buffered.len };
826 len += 1;
827 }
828 for (data[0 .. data.len - 1]) |d| {
829 if (d.len == 0) continue;
830 iovecs[len] = .{ .base = d.ptr, .len = d.len };
831 len += 1;
832 if (iovecs.len - len == 0) break;
833 }
834 const pattern = data[data.len - 1];
835 if (iovecs.len - len != 0) switch (splat) {
836 0 => {},
837 1 => if (pattern.len != 0) {
838 iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
839 len += 1;
840 },
841 else => switch (pattern.len) {
842 0 => {},
843 1 => {
844 const splat_buffer_candidate = io_w.buffer[io_w.end..];
845 var backup_buffer: [64]u8 = undefined;
846 const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len)
847 splat_buffer_candidate
848 else
849 &backup_buffer;
850 const memset_len = @min(splat_buffer.len, splat);
851 const buf = splat_buffer[0..memset_len];
852 @memset(buf, pattern[0]);
853 iovecs[len] = .{ .base = buf.ptr, .len = buf.len };
854 len += 1;
855 var remaining_splat = splat - buf.len;
856 while (remaining_splat > splat_buffer.len and iovecs.len - len != 0) {
857 assert(buf.len == splat_buffer.len);
858 iovecs[len] = .{ .base = splat_buffer.ptr, .len = splat_buffer.len };
859 len += 1;
860 remaining_splat -= splat_buffer.len;
861 }
862 if (remaining_splat > 0 and iovecs.len - len != 0) {
863 iovecs[len] = .{ .base = splat_buffer.ptr, .len = remaining_splat };
864 len += 1;
865 }
866 },
867 else => for (0..splat) |_| {
868 iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
869 len += 1;
870 if (iovecs.len - len == 0) break;
871 },
872 },
873 };
874 if (len == 0) return 0;
875 switch (w.mode) {
876 .positional, .positional_reading => {
877 const n = std.posix.pwritev(handle, iovecs[0..len], w.pos) catch |err| switch (err) {
878 error.Unseekable => {
879 w.mode = w.mode.toStreaming();
880 const pos = w.pos;
881 if (pos != 0) {
882 w.pos = 0;
883 w.seekTo(@intCast(pos)) catch {
884 w.mode = .failure;
885 return error.WriteFailed;
886 };
887 }
888 return 0;
889 },
890 else => |e| {
891 w.err = e;
892 return error.WriteFailed;
893 },
894 };
895 w.pos += n;
896 return io_w.consume(n);
897 },
898 .streaming, .streaming_reading => {
899 const n = std.posix.writev(handle, iovecs[0..len]) catch |err| {
900 w.err = err;
901 return error.WriteFailed;
902 };
903 w.pos += n;
904 return io_w.consume(n);
905 },
906 .failure => return error.WriteFailed,
907 }
908 }
909
910 pub fn sendFile(
911 io_w: *Io.Writer,
912 file_reader: *Io.File.Reader,
913 limit: Io.Limit,
914 ) Io.Writer.FileError!usize {
915 const reader_buffered = file_reader.interface.buffered();
916 if (reader_buffered.len >= @intFromEnum(limit))
917 return sendFileBuffered(io_w, file_reader, limit.slice(reader_buffered));
918 const writer_buffered = io_w.buffered();
919 const file_limit = @intFromEnum(limit) - reader_buffered.len;
920 const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
921 const out_fd = w.file.handle;
922 const in_fd = file_reader.file.handle;
923
924 if (file_reader.size) |size| {
925 if (size - file_reader.pos == 0) {
926 if (reader_buffered.len != 0) {
927 return sendFileBuffered(io_w, file_reader, reader_buffered);
928 } else {
929 return error.EndOfStream;
930 }
931 }
932 }
933
934 if (native_os == .freebsd and w.mode == .streaming) sf: {
935 // Try using sendfile on FreeBSD.
936 if (w.sendfile_err != null) break :sf;
937 const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
938 var hdtr_data: std.c.sf_hdtr = undefined;
939 var headers: [2]posix.iovec_const = undefined;
940 var headers_i: u8 = 0;
941 if (writer_buffered.len != 0) {
942 headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
943 headers_i += 1;
944 }
945 if (reader_buffered.len != 0) {
946 headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
947 headers_i += 1;
948 }
949 const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
950 hdtr_data = .{
951 .headers = &headers,
952 .hdr_cnt = headers_i,
953 .trailers = null,
954 .trl_cnt = 0,
955 };
956 break :b &hdtr_data;
957 };
958 var sbytes: std.c.off_t = undefined;
959 const nbytes: usize = @min(file_limit, maxInt(usize));
960 const flags = 0;
961 switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
962 .SUCCESS, .INTR => {},
963 .INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
964 .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
965 w.sendfile_err = error.Unexpected;
966 },
967 .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
968 w.sendfile_err = error.Unexpected;
969 },
970 .NOTCONN => w.sendfile_err = error.BrokenPipe,
971 .AGAIN, .BUSY => if (sbytes == 0) {
972 w.sendfile_err = error.WouldBlock;
973 },
974 .IO => w.sendfile_err = error.InputOutput,
975 .PIPE => w.sendfile_err = error.BrokenPipe,
976 .NOBUFS => w.sendfile_err = error.SystemResources,
977 else => |err| w.sendfile_err = posix.unexpectedErrno(err),
978 }
979 if (w.sendfile_err != null) {
980 // Give calling code chance to observe the error before trying
981 // something else.
982 return 0;
983 }
984 if (sbytes == 0) {
985 file_reader.size = file_reader.pos;
986 return error.EndOfStream;
987 }
988 const consumed = io_w.consume(@intCast(sbytes));
989 file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
990 return consumed;
991 }
992
993 if (native_os.isDarwin() and w.mode == .streaming) sf: {
994 // Try using sendfile on macOS.
995 if (w.sendfile_err != null) break :sf;
996 const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
997 var hdtr_data: std.c.sf_hdtr = undefined;
998 var headers: [2]posix.iovec_const = undefined;
999 var headers_i: u8 = 0;
1000 if (writer_buffered.len != 0) {
1001 headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
1002 headers_i += 1;
1003 }
1004 if (reader_buffered.len != 0) {
1005 headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
1006 headers_i += 1;
1007 }
1008 const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
1009 hdtr_data = .{
1010 .headers = &headers,
1011 .hdr_cnt = headers_i,
1012 .trailers = null,
1013 .trl_cnt = 0,
1014 };
1015 break :b &hdtr_data;
1016 };
1017 const max_count = maxInt(i32); // Avoid EINVAL.
1018 var len: std.c.off_t = @min(file_limit, max_count);
1019 const flags = 0;
1020 switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
1021 .SUCCESS, .INTR => {},
1022 .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
1023 .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
1024 w.sendfile_err = error.Unexpected;
1025 },
1026 .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
1027 w.sendfile_err = error.Unexpected;
1028 },
1029 .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
1030 w.sendfile_err = error.Unexpected;
1031 },
1032 .NOTCONN => w.sendfile_err = error.BrokenPipe,
1033 .AGAIN => if (len == 0) {
1034 w.sendfile_err = error.WouldBlock;
1035 },
1036 .IO => w.sendfile_err = error.InputOutput,
1037 .PIPE => w.sendfile_err = error.BrokenPipe,
1038 else => |err| w.sendfile_err = posix.unexpectedErrno(err),
1039 }
1040 if (w.sendfile_err != null) {
1041 // Give calling code chance to observe the error before trying
1042 // something else.
1043 return 0;
1044 }
1045 if (len == 0) {
1046 file_reader.size = file_reader.pos;
1047 return error.EndOfStream;
1048 }
1049 const consumed = io_w.consume(@bitCast(len));
1050 file_reader.seekBy(@intCast(consumed)) catch return error.ReadFailed;
1051 return consumed;
1052 }
1053
1054 if (native_os == .linux and w.mode == .streaming) sf: {
1055 // Try using sendfile on Linux.
1056 if (w.sendfile_err != null) break :sf;
1057 // Linux sendfile does not support headers.
1058 if (writer_buffered.len != 0 or reader_buffered.len != 0)
1059 return sendFileBuffered(io_w, file_reader, reader_buffered);
1060 const max_count = 0x7ffff000; // Avoid EINVAL.
1061 var off: std.os.linux.off_t = undefined;
1062 const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
1063 .positional => o: {
1064 const size = file_reader.getSize() catch return 0;
1065 off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
1066 break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
1067 },
1068 .streaming => .{ null, limit.minInt(max_count) },
1069 .streaming_reading, .positional_reading => break :sf,
1070 .failure => return error.ReadFailed,
1071 };
1072 const n = std.os.linux.wrapped.sendfile(out_fd, in_fd, off_ptr, count) catch |err| switch (err) {
1073 error.Unseekable => {
1074 file_reader.mode = file_reader.mode.toStreaming();
1075 const pos = file_reader.pos;
1076 if (pos != 0) {
1077 file_reader.pos = 0;
1078 file_reader.seekBy(@intCast(pos)) catch {
1079 file_reader.mode = .failure;
1080 return error.ReadFailed;
1081 };
1082 }
1083 return 0;
1084 },
1085 else => |e| {
1086 w.sendfile_err = e;
1087 return 0;
1088 },
1089 };
1090 if (n == 0) {
1091 file_reader.size = file_reader.pos;
1092 return error.EndOfStream;
1093 }
1094 file_reader.pos += n;
1095 w.pos += n;
1096 return n;
1097 }
1098
1099 const copy_file_range = switch (native_os) {
1100 .freebsd => std.os.freebsd.copy_file_range,
1101 .linux => std.os.linux.wrapped.copy_file_range,
1102 else => {},
1103 };
1104 if (@TypeOf(copy_file_range) != void) cfr: {
1105 if (w.copy_file_range_err != null) break :cfr;
1106 if (writer_buffered.len != 0 or reader_buffered.len != 0)
1107 return sendFileBuffered(io_w, file_reader, reader_buffered);
1108 var off_in: i64 = undefined;
1109 var off_out: i64 = undefined;
1110 const off_in_ptr: ?*i64 = switch (file_reader.mode) {
1111 .positional_reading, .streaming_reading => return error.Unimplemented,
1112 .positional => p: {
1113 off_in = @intCast(file_reader.pos);
1114 break :p &off_in;
1115 },
1116 .streaming => null,
1117 .failure => return error.WriteFailed,
1118 };
1119 const off_out_ptr: ?*i64 = switch (w.mode) {
1120 .positional_reading, .streaming_reading => return error.Unimplemented,
1121 .positional => p: {
1122 off_out = @intCast(w.pos);
1123 break :p &off_out;
1124 },
1125 .streaming => null,
1126 .failure => return error.WriteFailed,
1127 };
1128 const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
1129 w.copy_file_range_err = err;
1130 return 0;
1131 };
1132 if (n == 0) {
1133 file_reader.size = file_reader.pos;
1134 return error.EndOfStream;
1135 }
1136 file_reader.pos += n;
1137 w.pos += n;
1138 return n;
1139 }
1140
1141 if (builtin.os.tag.isDarwin()) fcf: {
1142 if (w.fcopyfile_err != null) break :fcf;
1143 if (file_reader.pos != 0) break :fcf;
1144 if (w.pos != 0) break :fcf;
1145 if (limit != .unlimited) break :fcf;
1146 const size = file_reader.getSize() catch break :fcf;
1147 if (writer_buffered.len != 0 or reader_buffered.len != 0)
1148 return sendFileBuffered(io_w, file_reader, reader_buffered);
1149 const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
1150 switch (posix.errno(rc)) {
1151 .SUCCESS => {},
1152 .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
1153 w.fcopyfile_err = error.Unexpected;
1154 return 0;
1155 },
1156 .NOMEM => {
1157 w.fcopyfile_err = error.OutOfMemory;
1158 return 0;
1159 },
1160 .OPNOTSUPP => {
1161 w.fcopyfile_err = error.OperationNotSupported;
1162 return 0;
1163 },
1164 else => |err| {
1165 w.fcopyfile_err = posix.unexpectedErrno(err);
1166 return 0;
1167 },
1168 }
1169 file_reader.pos = size;
1170 w.pos = size;
1171 return size;
1172 }
1173
1174 return error.Unimplemented;
1175 }
1176
1177 fn sendFileBuffered(
1178 io_w: *Io.Writer,
1179 file_reader: *Io.File.Reader,
1180 reader_buffered: []const u8,
1181 ) Io.Writer.FileError!usize {
1182 const n = try drain(io_w, &.{reader_buffered}, 1);
1183 file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
1184 return n;
1185 }
1186
1187 pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
1188 try w.interface.flush();
1189 try seekToUnbuffered(w, offset);
1190 }
1191
1192 /// Asserts that no data is currently buffered.
1193 pub fn seekToUnbuffered(w: *Writer, offset: u64) Writer.SeekError!void {
1194 assert(w.interface.buffered().len == 0);
1195 switch (w.mode) {
1196 .positional, .positional_reading => {
1197 w.pos = offset;
1198 },
1199 .streaming, .streaming_reading => {
1200 if (w.seek_err) |err| return err;
1201 posix.lseek_SET(w.file.handle, offset) catch |err| {
1202 w.seek_err = err;
1203 return err;
1204 };
1205 w.pos = offset;
1206 },
1207 .failure => return w.seek_err.?,
1208 }
1209 }
1210
1211 pub const EndError = SetEndPosError || Io.Writer.Error;
1212
1213 /// Flushes any buffered data and sets the end position of the file.
1214 ///
1215 /// If not overwriting existing contents, then calling `interface.flush`
1216 /// directly is sufficient.
1217 ///
1218 /// Flush failure is handled by setting `err` so that it can be handled
1219 /// along with other write failures.
1220 pub fn end(w: *Writer) EndError!void {
1221 try w.interface.flush();
1222 switch (w.mode) {
1223 .positional,
1224 .positional_reading,
1225 => w.file.setEndPos(w.pos) catch |err| switch (err) {
1226 error.NonResizable => return,
1227 else => |e| return e,
1228 },
1229
1230 .streaming,
1231 .streaming_reading,
1232 .failure,
1233 => {},
1234 }
1235 }
1236};
1237
1238/// Defaults to positional reading; falls back to streaming.
1239///
1240/// Positional is more threadsafe, since the global seek position is not
1241/// affected.
1242pub fn reader(file: File, io: Io, buffer: []u8) Reader {
1243 return .init(.{ .handle = file.handle }, io, buffer);
1244}
1245
1246/// Positional is more threadsafe, since the global seek position is not
1247/// affected, but when such syscalls are not available, preemptively
1248/// initializing in streaming mode skips a failed syscall.
1249pub fn readerStreaming(file: File, io: Io, buffer: []u8) Reader {
1250 return .initStreaming(.{ .handle = file.handle }, io, buffer);
1251}
1252
1253/// Defaults to positional reading; falls back to streaming.
1254///
1255/// Positional is more threadsafe, since the global seek position is not
1256/// affected.
1257pub fn writer(file: File, buffer: []u8) Writer {
1258 return .init(file, buffer);
1259}
1260
1261/// Positional is more threadsafe, since the global seek position is not
1262/// affected, but when such syscalls are not available, preemptively
1263/// initializing in streaming mode will skip a failed syscall.
1264pub fn writerStreaming(file: File, buffer: []u8) Writer {
1265 return .initStreaming(file, buffer);
1266}
1267
1268const range_off: windows.LARGE_INTEGER = 0;
1269const range_len: windows.LARGE_INTEGER = 1;
1270
1271pub const LockError = error{
1272 SystemResources,
1273 FileLocksNotSupported,
1274} || posix.UnexpectedError;
1275
1276/// Blocks when an incompatible lock is held by another process.
1277/// A process may hold only one type of lock (shared or exclusive) on
1278/// a file. When a process terminates in any way, the lock is released.
1279///
1280/// Assumes the file is unlocked.
1281///
1282/// TODO: integrate with async I/O
1283pub fn lock(file: File, l: Lock) LockError!void {
1284 if (is_windows) {
1285 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1286 const exclusive = switch (l) {
1287 .none => return,
1288 .shared => false,
1289 .exclusive => true,
1290 };
1291 return windows.LockFile(
1292 file.handle,
1293 null,
1294 null,
1295 null,
1296 &io_status_block,
1297 &range_off,
1298 &range_len,
1299 null,
1300 windows.FALSE, // non-blocking=false
1301 @intFromBool(exclusive),
1302 ) catch |err| switch (err) {
1303 error.WouldBlock => unreachable, // non-blocking=false
1304 else => |e| return e,
1305 };
1306 } else {
1307 return posix.flock(file.handle, switch (l) {
1308 .none => posix.LOCK.UN,
1309 .shared => posix.LOCK.SH,
1310 .exclusive => posix.LOCK.EX,
1311 }) catch |err| switch (err) {
1312 error.WouldBlock => unreachable, // non-blocking=false
1313 else => |e| return e,
1314 };
1315 }
1316}
1317
1318/// Assumes the file is locked.
1319pub fn unlock(file: File) void {
1320 if (is_windows) {
1321 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1322 return windows.UnlockFile(
1323 file.handle,
1324 &io_status_block,
1325 &range_off,
1326 &range_len,
1327 null,
1328 ) catch |err| switch (err) {
1329 error.RangeNotLocked => unreachable, // Function assumes unlocked.
1330 error.Unexpected => unreachable, // Resource deallocation must succeed.
1331 };
1332 } else {
1333 return posix.flock(file.handle, posix.LOCK.UN) catch |err| switch (err) {
1334 error.WouldBlock => unreachable, // unlocking can't block
1335 error.SystemResources => unreachable, // We are deallocating resources.
1336 error.FileLocksNotSupported => unreachable, // We already got the lock.
1337 error.Unexpected => unreachable, // Resource deallocation must succeed.
1338 };
1339 }
1340}
1341
1342/// Attempts to obtain a lock, returning `true` if the lock is
1343/// obtained, and `false` if there was an existing incompatible lock held.
1344/// A process may hold only one type of lock (shared or exclusive) on
1345/// a file. When a process terminates in any way, the lock is released.
1346///
1347/// Assumes the file is unlocked.
1348///
1349/// TODO: integrate with async I/O
1350pub fn tryLock(file: File, l: Lock) LockError!bool {
1351 if (is_windows) {
1352 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1353 const exclusive = switch (l) {
1354 .none => return,
1355 .shared => false,
1356 .exclusive => true,
1357 };
1358 windows.LockFile(
1359 file.handle,
1360 null,
1361 null,
1362 null,
1363 &io_status_block,
1364 &range_off,
1365 &range_len,
1366 null,
1367 windows.TRUE, // non-blocking=true
1368 @intFromBool(exclusive),
1369 ) catch |err| switch (err) {
1370 error.WouldBlock => return false,
1371 else => |e| return e,
1372 };
1373 } else {
1374 posix.flock(file.handle, switch (l) {
1375 .none => posix.LOCK.UN,
1376 .shared => posix.LOCK.SH | posix.LOCK.NB,
1377 .exclusive => posix.LOCK.EX | posix.LOCK.NB,
1378 }) catch |err| switch (err) {
1379 error.WouldBlock => return false,
1380 else => |e| return e,
1381 };
1382 }
1383 return true;
1384}
1385
1386/// Assumes the file is already locked in exclusive mode.
1387/// Atomically modifies the lock to be in shared mode, without releasing it.
1388///
1389/// TODO: integrate with async I/O
1390pub fn downgradeLock(file: File) LockError!void {
1391 if (is_windows) {
1392 // On Windows it works like a semaphore + exclusivity flag. To implement this
1393 // function, we first obtain another lock in shared mode. This changes the
1394 // exclusivity flag, but increments the semaphore to 2. So we follow up with
1395 // an NtUnlockFile which decrements the semaphore but does not modify the
1396 // exclusivity flag.
1397 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
1398 windows.LockFile(
1399 file.handle,
1400 null,
1401 null,
1402 null,
1403 &io_status_block,
1404 &range_off,
1405 &range_len,
1406 null,
1407 windows.TRUE, // non-blocking=true
1408 windows.FALSE, // exclusive=false
1409 ) catch |err| switch (err) {
1410 error.WouldBlock => unreachable, // File was not locked in exclusive mode.
1411 else => |e| return e,
1412 };
1413 return windows.UnlockFile(
1414 file.handle,
1415 &io_status_block,
1416 &range_off,
1417 &range_len,
1418 null,
1419 ) catch |err| switch (err) {
1420 error.RangeNotLocked => unreachable, // File was not locked.
1421 error.Unexpected => unreachable, // Resource deallocation must succeed.
1422 };
1423 } else {
1424 return posix.flock(file.handle, posix.LOCK.SH | posix.LOCK.NB) catch |err| switch (err) {
1425 error.WouldBlock => unreachable, // File was not locked in exclusive mode.
1426 else => |e| return e,
1427 };
1428 }
1429}
1430
1431pub fn adaptToNewApi(file: File) Io.File {
1432 return .{ .handle = file.handle };
1433}
1434
1435pub fn adaptFromNewApi(file: Io.File) File {
1436 return .{ .handle = file.handle };
1437}