authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-06 05:32:16+01:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-06 05:32:16+01:00
logc906f7d2e7d66bdc07fe8815d1fb2bb3d6664478
tree13057f5eec03fdb2182cc012e6b1b2941ae30caa
parent6ab1159e815fdc7ce11fa49235509a43da74f899
parent06130c5e61160b20305c9e6a621fa1c4d0df7858

Merge pull request 'std: rework atomic file / temp file API' (#30686) from std.Io.File.Atomic into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/30686

13 files changed, 450 insertions(+), 207 deletions(-)

lib/std/Io.zig+2
......@@ -667,6 +667,7 @@ pub const VTable = struct {
667667 dirStatFile: *const fn (?*anyopaque, Dir, []const u8, Dir.StatFileOptions) Dir.StatFileError!File.Stat,
668668 dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void,
669669 dirCreateFile: *const fn (?*anyopaque, Dir, []const u8, File.CreateFlags) File.OpenError!File,
670 dirCreateFileAtomic: *const fn (?*anyopaque, Dir, []const u8, Dir.CreateFileAtomicOptions) Dir.CreateFileAtomicError!File.Atomic,
670671 dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File,
671672 dirClose: *const fn (?*anyopaque, []const Dir) void,
672673 dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize,
......@@ -710,6 +711,7 @@ pub const VTable = struct {
710711 fileUnlock: *const fn (?*anyopaque, File) void,
711712 fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,
712713 fileRealPath: *const fn (?*anyopaque, File, out_buffer: []u8) File.RealPathError!usize,
714 fileHardLink: *const fn (?*anyopaque, File, Dir, []const u8, File.HardLinkOptions) File.HardLinkError!void,
713715
714716 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
715717 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,
lib/std/Io/Dir.zig+90-72
......@@ -454,7 +454,6 @@ pub const OpenError = error{
454454 SystemFdQuotaExceeded,
455455 NoDevice,
456456 SystemResources,
457 DeviceBusy,
458457 /// On Windows, `\\server` or `\\server\share` was not found.
459458 NetworkNotFound,
460459} || PathNameError || Io.Cancelable || Io.UnexpectedError;
......@@ -598,30 +597,29 @@ pub fn updateFile(
598597 }
599598 }
600599
601 if (path.dirname(dest_path)) |dirname| {
602 try dest_dir.createDirPath(io, dirname);
603 }
604
605 var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
606 var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
600 var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
607601 .permissions = actual_permissions,
608 .write_buffer = &buffer,
602 .make_path = true,
603 .replace = true,
609604 });
610 defer atomic_file.deinit();
605 defer atomic_file.deinit(io);
606
607 var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
608 var file_writer = atomic_file.file.writer(io, &buffer);
611609
612610 var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size);
613 const dest_writer = &atomic_file.file_writer.interface;
611 const dest_writer = &file_writer.interface;
614612
615613 _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
616614 error.ReadFailed => return src_reader.err.?,
617 error.WriteFailed => return atomic_file.file_writer.err.?,
615 error.WriteFailed => return file_writer.err.?,
618616 };
619 try atomic_file.flush();
620 try atomic_file.file_writer.file.setTimestamps(io, .{
617 try file_writer.flush();
618 try file_writer.file.setTimestamps(io, .{
621619 .access_timestamp = .init(src_stat.atime),
622620 .modify_timestamp = .init(src_stat.mtime),
623621 });
624 try atomic_file.renameIntoPlace();
622 try atomic_file.replace(io);
625623 return .stale;
626624}
627625
......@@ -995,27 +993,9 @@ pub fn renameAbsolute(old_path: []const u8, new_path: []const u8, io: Io) Rename
995993 return io.vtable.dirRename(io.userdata, my_cwd, old_path, my_cwd, new_path);
996994}
997995
998pub const HardLinkOptions = struct {
999 follow_symlinks: bool = true,
1000};
996pub const HardLinkOptions = File.HardLinkOptions;
1001997
1002pub const HardLinkError = error{
1003 AccessDenied,
1004 PermissionDenied,
1005 DiskQuota,
1006 PathAlreadyExists,
1007 HardwareFailure,
1008 /// Either the OS or the filesystem does not support hard links.
1009 OperationUnsupported,
1010 SymLinkLoop,
1011 LinkQuotaExceeded,
1012 FileNotFound,
1013 SystemResources,
1014 NoSpaceLeft,
1015 ReadOnlyFileSystem,
1016 NotSameFileSystem,
1017 NotDir,
1018} || Io.Cancelable || PathNameError || Io.UnexpectedError;
998pub const HardLinkError = File.HardLinkError;
1019999
10201000pub fn hardLink(
10211001 old_dir: Dir,
......@@ -1251,7 +1231,6 @@ pub const DeleteTreeError = error{
12511231 ReadOnlyFileSystem,
12521232 FileSystem,
12531233 FileBusy,
1254 DeviceBusy,
12551234 /// One of the path components was not a directory.
12561235 /// This error is unreachable if `sub_path` does not contain a path separator.
12571236 NotDir,
......@@ -1322,7 +1301,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
13221301 error.Unexpected,
13231302 error.BadPathName,
13241303 error.NetworkNotFound,
1325 error.DeviceBusy,
13261304 error.Canceled,
13271305 => |e| return e,
13281306 };
......@@ -1417,7 +1395,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
14171395 error.Unexpected,
14181396 error.BadPathName,
14191397 error.NetworkNotFound,
1420 error.DeviceBusy,
14211398 error.Canceled,
14221399 => |e| return e,
14231400 };
......@@ -1522,7 +1499,6 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8,
15221499 error.Unexpected,
15231500 error.BadPathName,
15241501 error.NetworkNotFound,
1525 error.DeviceBusy,
15261502 error.Canceled,
15271503 => |e| return e,
15281504 };
......@@ -1619,7 +1595,6 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
16191595 error.SystemResources,
16201596 error.Unexpected,
16211597 error.BadPathName,
1622 error.DeviceBusy,
16231598 error.NetworkNotFound,
16241599 error.Canceled,
16251600 => |e| return e,
......@@ -1658,15 +1633,18 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
16581633pub const CopyFileOptions = struct {
16591634 /// When this is `null` the permissions are copied from the source file.
16601635 permissions: ?File.Permissions = null,
1636 make_path: bool = false,
1637 replace: bool = true,
16611638};
16621639
16631640pub const CopyFileError = File.OpenError || File.StatError ||
1664 File.Atomic.InitError || File.Atomic.FinishError ||
1641 CreateFileAtomicError || File.Atomic.ReplaceError || File.Atomic.LinkError ||
16651642 File.Reader.Error || File.Writer.Error || error{InvalidFileName};
16661643
16671644/// Atomically creates a new file at `dest_path` within `dest_dir` with the
1668/// same contents as `source_path` within `source_dir`, overwriting any already
1669/// existing file.
1645/// same contents as `source_path` within `source_dir`.
1646///
1647/// Whether to overwrite the existing file is determined by `options`.
16701648///
16711649/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and
16721650/// readily available, there is a possibility of power loss or application
......@@ -1695,19 +1673,27 @@ pub fn copyFile(
16951673 break :blk st.permissions;
16961674 };
16971675
1698 var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
1699 var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
1676 var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
17001677 .permissions = permissions,
1701 .write_buffer = &buffer,
1678 .make_path = options.make_path,
1679 .replace = options.replace,
17021680 });
1703 defer atomic_file.deinit();
1681 defer atomic_file.deinit(io);
17041682
1705 _ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
1683 var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
1684 var file_writer = atomic_file.file.writer(io, &buffer);
1685
1686 _ = file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
17061687 error.ReadFailed => return file_reader.err.?,
1707 error.WriteFailed => return atomic_file.file_writer.err.?,
1688 error.WriteFailed => return file_writer.err.?,
17081689 };
17091690
1710 try atomic_file.finish();
1691 try file_writer.flush();
1692
1693 switch (options.replace) {
1694 true => try atomic_file.replace(io),
1695 false => try atomic_file.link(io),
1696 }
17111697}
17121698
17131699/// Same as `copyFile`, except asserts that both `source_path` and `dest_path`
......@@ -1730,33 +1716,65 @@ pub fn copyFileAbsolute(
17301716
17311717test copyFileAbsolute {}
17321718
1733pub const AtomicFileOptions = struct {
1719pub const CreateFileAtomicOptions = struct {
17341720 permissions: File.Permissions = .default_file,
17351721 make_path: bool = false,
1736 write_buffer: []u8,
1722 /// Tells whether the unnamed file will be ultimately created with
1723 /// `File.Atomic.link` or `File.Atomic.replace`.
1724 ///
1725 /// If this value is incorrect it will cause an assertion failure in
1726 /// `File.Atomic.replace`.
1727 replace: bool = false,
17371728};
17381729
1739/// Directly access the `.file` field, and then call `File.Atomic.finish` to
1740/// atomically replace `dest_path` with contents.
1741///
1742/// Always call `File.Atomic.deinit` to clean up, regardless of whether
1743/// `File.Atomic.finish` succeeded. `dest_path` must remain valid until
1744/// `File.Atomic.deinit` is called.
1745///
1746/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
1747/// On WASI, `dest_path` should be encoded as valid UTF-8.
1748/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding.
1749pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic {
1750 if (path.dirname(dest_path)) |dirname| {
1751 const dir = if (options.make_path)
1752 try parent.createDirPathOpen(io, dirname, .{})
1753 else
1754 try parent.openDir(io, dirname, .{});
1755
1756 return .init(io, path.basename(dest_path), options.permissions, dir, true, options.write_buffer);
1757 } else {
1758 return .init(io, dest_path, options.permissions, parent, false, options.write_buffer);
1759 }
1730pub const CreateFileAtomicError = error{
1731 NoDevice,
1732 /// On Windows, `\\server` or `\\server\share` was not found.
1733 NetworkNotFound,
1734 /// On Windows, antivirus software is enabled by default. It can be
1735 /// disabled, but Windows Update sometimes ignores the user's preference
1736 /// and re-enables it. When enabled, antivirus software on Windows
1737 /// intercepts file system operations and makes them significantly slower
1738 /// in addition to possibly failing with this error code.
1739 AntivirusInterference,
1740 /// In WASI, this error may occur when the file descriptor does
1741 /// not hold the required rights to open a new resource relative to it.
1742 AccessDenied,
1743 PermissionDenied,
1744 SymLinkLoop,
1745 ProcessFdQuotaExceeded,
1746 SystemFdQuotaExceeded,
1747 /// Either:
1748 /// * One of the path components does not exist.
1749 /// * Cwd was used, but cwd has been deleted.
1750 /// * The path associated with the open directory handle has been deleted.
1751 FileNotFound,
1752 /// Insufficient kernel memory was available.
1753 SystemResources,
1754 /// A new path cannot be created because the device has no room for the new file.
1755 NoSpaceLeft,
1756 /// A component used as a directory in the path was not, in fact, a directory.
1757 NotDir,
1758 WouldBlock,
1759 ReadOnlyFileSystem,
1760} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
1761
1762/// Create an unnamed ephemeral file that can eventually be atomically
1763/// materialized into `sub_path`.
1764///
1765/// The returned `File.Atomic` provides API to emulate the behavior in case it
1766/// is not directly supported by the underlying operating system.
1767///
1768/// * On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).
1769/// * On WASI, `sub_path` should be encoded as valid UTF-8.
1770/// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
1771pub fn createFileAtomic(
1772 dir: Dir,
1773 io: Io,
1774 sub_path: []const u8,
1775 options: CreateFileAtomicOptions,
1776) CreateFileAtomicError!File.Atomic {
1777 return io.vtable.dirCreateFileAtomic(io.userdata, dir, sub_path, options);
17601778}
17611779
17621780pub const SetPermissionsError = File.SetPermissionsError;
lib/std/Io/File.zig+33-1
......@@ -278,7 +278,7 @@ pub const OpenError = error{
278278 FileBusy,
279279 /// Non-blocking was requested and the operation cannot return immediately.
280280 WouldBlock,
281} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
281} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
282282
283283pub fn close(file: File, io: Io) void {
284284 return io.vtable.fileClose(io.userdata, (&file)[0..1]);
......@@ -708,6 +708,38 @@ pub fn realPath(file: File, io: Io, out_buffer: []u8) RealPathError!usize {
708708 return io.vtable.fileRealPath(io.userdata, file, out_buffer);
709709}
710710
711pub const HardLinkOptions = struct {
712 follow_symlinks: bool = true,
713};
714
715pub const HardLinkError = error{
716 AccessDenied,
717 PermissionDenied,
718 DiskQuota,
719 PathAlreadyExists,
720 HardwareFailure,
721 /// Either the OS or the filesystem does not support hard links.
722 OperationUnsupported,
723 SymLinkLoop,
724 LinkQuotaExceeded,
725 FileNotFound,
726 SystemResources,
727 NoSpaceLeft,
728 ReadOnlyFileSystem,
729 NotSameFileSystem,
730 NotDir,
731} || Io.Cancelable || Dir.PathNameError || Io.UnexpectedError;
732
733pub fn hardLink(
734 file: File,
735 io: Io,
736 new_dir: Dir,
737 new_sub_path: []const u8,
738 options: HardLinkOptions,
739) HardLinkError!void {
740 return io.vtable.fileHardLink(io.userdata, file, new_dir, new_sub_path, options);
741}
742
711743test {
712744 _ = Reader;
713745 _ = Writer;
lib/std/Io/File/Atomic.zig+43-62
......@@ -6,97 +6,78 @@ const File = std.Io.File;
66const Dir = std.Io.Dir;
77const assert = std.debug.assert;
88
9file_writer: File.Writer,
10random_integer: u64,
11dest_basename: []const u8,
9file: File,
10file_basename_hex: u64,
1211file_open: bool,
1312file_exists: bool,
14close_dir_on_deinit: bool,
13
1514dir: Dir,
15close_dir_on_deinit: bool,
1616
17pub const InitError = File.OpenError;
17dest_sub_path: []const u8,
1818
19/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
20pub fn init(
21 io: Io,
22 dest_basename: []const u8,
23 permissions: File.Permissions,
24 dir: Dir,
25 close_dir_on_deinit: bool,
26 write_buffer: []u8,
27) InitError!Atomic {
28 while (true) {
29 const random_integer = std.crypto.random.int(u64);
30 const tmp_sub_path = std.fmt.hex(random_integer);
31 const file = dir.createFile(io, &tmp_sub_path, .{
32 .permissions = permissions,
33 .exclusive = true,
34 }) catch |err| switch (err) {
35 error.PathAlreadyExists => continue,
36 else => |e| return e,
37 };
38 return .{
39 .file_writer = file.writer(io, write_buffer),
40 .random_integer = random_integer,
41 .dest_basename = dest_basename,
42 .file_open = true,
43 .file_exists = true,
44 .close_dir_on_deinit = close_dir_on_deinit,
45 .dir = dir,
46 };
47 }
48}
49
50/// Always call deinit, even after a successful finish().
51pub fn deinit(af: *Atomic) void {
52 const io = af.file_writer.io;
19pub const InitError = File.OpenError;
5320
21/// To release all resources, always call `deinit`, even after a successful
22/// `finish`.
23pub fn deinit(af: *Atomic, io: Io) void {
5424 if (af.file_open) {
55 af.file_writer.file.close(io);
25 af.file.close(io);
5626 af.file_open = false;
5727 }
5828 if (af.file_exists) {
59 const tmp_sub_path = std.fmt.hex(af.random_integer);
29 const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
6030 af.dir.deleteFile(io, &tmp_sub_path) catch {};
6131 af.file_exists = false;
6232 }
6333 if (af.close_dir_on_deinit) {
6434 af.dir.close(io);
35 af.close_dir_on_deinit = false;
6536 }
6637 af.* = undefined;
6738}
6839
69pub const FlushError = File.Writer.Error;
40pub const LinkError = Dir.HardLinkError;
7041
71pub fn flush(af: *Atomic) FlushError!void {
72 af.file_writer.interface.flush() catch |err| switch (err) {
73 error.WriteFailed => return af.file_writer.err.?,
74 };
42/// Atomically materializes the file into place, failing with
43/// `error.PathAlreadyExists` if something already exists there.
44pub fn link(af: *Atomic, io: Io) LinkError!void {
45 if (af.file_exists) {
46 if (af.file_open) {
47 af.file.close(io);
48 af.file_open = false;
49 }
50 const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
51 try af.dir.hardLink(&tmp_sub_path, af.dir, af.dest_sub_path, io, .{});
52 af.dir.deleteFile(io, &tmp_sub_path) catch {};
53 af.file_exists = false;
54 } else {
55 assert(af.file_open);
56 try af.file.hardLink(io, af.dir, af.dest_sub_path, .{});
57 af.file.close(io);
58 af.file_open = false;
59 }
7560}
7661
77pub const RenameIntoPlaceError = Dir.RenameError;
62pub const ReplaceError = Dir.RenameError;
7863
64/// Atomically materializes the file into place, replacing any file that
65/// already exists there.
66///
67/// Calling this function requires setting `CreateFileAtomicOptions.replace` to
68/// `true`.
69///
7970/// On Windows, this function introduces a period of time where some file
8071/// system operations on the destination file will result in
8172/// `error.AccessDenied`, including rename operations (such as the one used in
8273/// this function).
83pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {
84 const io = af.file_writer.io;
85
86 assert(af.file_exists);
74pub fn replace(af: *Atomic, io: Io) ReplaceError!void {
75 assert(af.file_exists); // Wrong value for `CreateFileAtomicOptions.replace`.
8776 if (af.file_open) {
88 af.file_writer.file.close(io);
77 af.file.close(io);
8978 af.file_open = false;
9079 }
91 const tmp_sub_path = std.fmt.hex(af.random_integer);
92 try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);
80 const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
81 try af.dir.rename(&tmp_sub_path, af.dir, af.dest_sub_path, io);
9382 af.file_exists = false;
9483}
95
96pub const FinishError = FlushError || RenameIntoPlaceError;
97
98/// Combination of `flush` followed by `renameIntoPlace`.
99pub fn finish(af: *Atomic) FinishError!void {
100 try af.flush();
101 try af.renameIntoPlace();
102}
lib/std/Io/File/Writer.zig+8
......@@ -272,3 +272,11 @@ pub fn end(w: *Writer) EndError!void {
272272 => {},
273273 }
274274}
275
276/// Convenience method for calling `Io.Writer.flush` and returning the
277/// underlying error.
278pub fn flush(w: *Writer) Error!void {
279 w.interface.flush() catch |err| switch (err) {
280 error.WriteFailed => return w.err.?,
281 };
282}
lib/std/Io/Threaded.zig+228-43
......@@ -1403,6 +1403,7 @@ pub fn io(t: *Threaded) Io {
14031403 .dirStatFile = dirStatFile,
14041404 .dirAccess = dirAccess,
14051405 .dirCreateFile = dirCreateFile,
1406 .dirCreateFileAtomic = dirCreateFileAtomic,
14061407 .dirOpenFile = dirOpenFile,
14071408 .dirOpenDir = dirOpenDir,
14081409 .dirClose = dirClose,
......@@ -1445,6 +1446,7 @@ pub fn io(t: *Threaded) Io {
14451446 .fileUnlock = fileUnlock,
14461447 .fileDowngradeLock = fileDowngradeLock,
14471448 .fileRealPath = fileRealPath,
1449 .fileHardLink = fileHardLink,
14481450
14491451 .processExecutableOpen = processExecutableOpen,
14501452 .processExecutablePath = processExecutablePath,
......@@ -1549,6 +1551,7 @@ pub fn ioBasic(t: *Threaded) Io {
15491551 .dirStatFile = dirStatFile,
15501552 .dirAccess = dirAccess,
15511553 .dirCreateFile = dirCreateFile,
1554 .dirCreateFileAtomic = dirCreateFileAtomic,
15521555 .dirOpenFile = dirOpenFile,
15531556 .dirOpenDir = dirOpenDir,
15541557 .dirClose = dirClose,
......@@ -1591,6 +1594,7 @@ pub fn ioBasic(t: *Threaded) Io {
15911594 .fileUnlock = fileUnlock,
15921595 .fileDowngradeLock = fileDowngradeLock,
15931596 .fileRealPath = fileRealPath,
1597 .fileHardLink = fileHardLink,
15941598
15951599 .processExecutableOpen = processExecutableOpen,
15961600 .processExecutablePath = processExecutablePath,
......@@ -3413,6 +3417,170 @@ fn dirCreateFileWasi(
34133417 }
34143418}
34153419
3420fn dirCreateFileAtomic(
3421 userdata: ?*anyopaque,
3422 dir: Dir,
3423 dest_path: []const u8,
3424 options: Dir.CreateFileAtomicOptions,
3425) Dir.CreateFileAtomicError!File.Atomic {
3426 const t: *Threaded = @ptrCast(@alignCast(userdata));
3427 const t_io = ioBasic(t);
3428
3429 // Linux has O_TMPFILE, but linkat() does not support AT_REPLACE, so it's
3430 // useless when we have to make up a bogus path name to do the rename()
3431 // anyway.
3432 if (native_os == .linux and !options.replace) tmpfile: {
3433 const flags: posix.O = if (@hasField(posix.O, "TMPFILE")) .{
3434 .ACCMODE = .RDWR,
3435 .TMPFILE = true,
3436 .DIRECTORY = true,
3437 .CLOEXEC = true,
3438 } else if (@hasField(posix.O, "TMPFILE0") and !@hasField(posix.O, "TMPFILE2")) .{
3439 .ACCMODE = .RDWR,
3440 .TMPFILE0 = true,
3441 .TMPFILE1 = true,
3442 .DIRECTORY = true,
3443 .CLOEXEC = true,
3444 } else break :tmpfile;
3445
3446 const dest_dirname = Dir.path.dirname(dest_path);
3447 if (dest_dirname) |dirname| {
3448 // This has a nice side effect of preemptively triggering EISDIR or
3449 // ENOENT, avoiding the ambiguity below.
3450 dir.createDirPath(t_io, dirname) catch |err| switch (err) {
3451 // None of these make sense in this context.
3452 error.IsDir,
3453 error.Streaming,
3454 error.DiskQuota,
3455 error.PathAlreadyExists,
3456 error.LinkQuotaExceeded,
3457 error.SharingViolation,
3458 error.PipeBusy,
3459 error.FileTooBig,
3460 error.DeviceBusy,
3461 error.FileLocksUnsupported,
3462 error.FileBusy,
3463 => return error.Unexpected,
3464
3465 else => |e| return e,
3466 };
3467 }
3468
3469 var path_buffer: [posix.PATH_MAX]u8 = undefined;
3470 const sub_path_posix = try pathToPosix(dest_dirname orelse ".", &path_buffer);
3471
3472 const syscall: Syscall = try .start();
3473 while (true) {
3474 const rc = openat_sym(dir.handle, sub_path_posix, flags, options.permissions.toMode());
3475 switch (posix.errno(rc)) {
3476 .SUCCESS => {
3477 syscall.finish();
3478 return .{
3479 .file = .{ .handle = @intCast(rc) },
3480 .file_basename_hex = 0,
3481 .dest_sub_path = dest_path,
3482 .file_open = true,
3483 .file_exists = false,
3484 .close_dir_on_deinit = false,
3485 .dir = dir,
3486 };
3487 },
3488 .INTR => {
3489 try syscall.checkCancel();
3490 continue;
3491 },
3492 .ISDIR, .NOENT => {
3493 // Ambiguous error code. It might mean the file system
3494 // does not support O_TMPFILE. Therefore, we must fall
3495 // back to not using O_TMPFILE.
3496 syscall.finish();
3497 break :tmpfile;
3498 },
3499 .INVAL => return syscall.fail(error.BadPathName),
3500 .ACCES => return syscall.fail(error.AccessDenied),
3501 .LOOP => return syscall.fail(error.SymLinkLoop),
3502 .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded),
3503 .NAMETOOLONG => return syscall.fail(error.NameTooLong),
3504 .NFILE => return syscall.fail(error.SystemFdQuotaExceeded),
3505 .NODEV => return syscall.fail(error.NoDevice),
3506 .NOMEM => return syscall.fail(error.SystemResources),
3507 .NOSPC => return syscall.fail(error.NoSpaceLeft),
3508 .NOTDIR => return syscall.fail(error.NotDir),
3509 .PERM => return syscall.fail(error.PermissionDenied),
3510 .AGAIN => return syscall.fail(error.WouldBlock),
3511 .NXIO => return syscall.fail(error.NoDevice),
3512 .ILSEQ => return syscall.fail(error.BadPathName),
3513 else => |err| return syscall.unexpectedErrno(err),
3514 }
3515 }
3516 }
3517
3518 if (Dir.path.dirname(dest_path)) |dirname| {
3519 const new_dir = if (options.make_path)
3520 dir.createDirPathOpen(t_io, dirname, .{}) catch |err| switch (err) {
3521 // None of these make sense in this context.
3522 error.IsDir,
3523 error.Streaming,
3524 error.DiskQuota,
3525 error.PathAlreadyExists,
3526 error.LinkQuotaExceeded,
3527 error.SharingViolation,
3528 error.PipeBusy,
3529 error.FileTooBig,
3530 error.FileLocksUnsupported,
3531 error.FileBusy,
3532 error.DeviceBusy,
3533 => return error.Unexpected,
3534
3535 else => |e| return e,
3536 }
3537 else
3538 try dir.openDir(t_io, dirname, .{});
3539
3540 return atomicFileInit(t_io, Dir.path.basename(dest_path), options.permissions, new_dir, true);
3541 }
3542
3543 return atomicFileInit(t_io, dest_path, options.permissions, dir, false);
3544}
3545
3546fn atomicFileInit(
3547 t_io: Io,
3548 dest_basename: []const u8,
3549 permissions: File.Permissions,
3550 dir: Dir,
3551 close_dir_on_deinit: bool,
3552) Dir.CreateFileAtomicError!File.Atomic {
3553 while (true) {
3554 const random_integer = std.crypto.random.int(u64);
3555 const tmp_sub_path = std.fmt.hex(random_integer);
3556 const file = dir.createFile(t_io, &tmp_sub_path, .{
3557 .permissions = permissions,
3558 .exclusive = true,
3559 }) catch |err| switch (err) {
3560 error.PathAlreadyExists => continue,
3561 error.DeviceBusy => continue,
3562 error.FileBusy => continue,
3563 error.SharingViolation => continue,
3564
3565 error.IsDir => return error.Unexpected, // No path components.
3566 error.FileTooBig => return error.Unexpected, // Creating, not opening.
3567 error.FileLocksUnsupported => return error.Unexpected, // Not asking for locks.
3568 error.PipeBusy => return error.Unexpected, // Not opening a pipe.
3569
3570 else => |e| return e,
3571 };
3572 return .{
3573 .file = file,
3574 .file_basename_hex = random_integer,
3575 .dest_sub_path = dest_basename,
3576 .file_open = true,
3577 .file_exists = true,
3578 .close_dir_on_deinit = close_dir_on_deinit,
3579 .dir = dir,
3580 };
3581 }
3582}
3583
34163584const dirOpenFile = switch (native_os) {
34173585 .windows => dirOpenFileWindows,
34183586 .wasi => dirOpenFileWasi,
......@@ -3925,7 +4093,7 @@ fn dirOpenDirPosix(
39254093 .NOMEM => return error.SystemResources,
39264094 .NOTDIR => return error.NotDir,
39274095 .PERM => return error.PermissionDenied,
3928 .BUSY => return error.DeviceBusy,
4096 .BUSY => |err| return errnoBug(err), // O_EXCL not passed
39294097 .NXIO => return error.NoDevice,
39304098 .ILSEQ => return error.BadPathName,
39314099 else => |err| return posix.unexpectedErrno(err),
......@@ -4985,6 +5153,64 @@ fn realPathPosix(fd: posix.fd_t, out_buffer: []u8) File.RealPathError!usize {
49855153 comptime unreachable;
49865154}
49875155
5156fn fileHardLink(
5157 userdata: ?*anyopaque,
5158 file: File,
5159 new_dir: Dir,
5160 new_sub_path: []const u8,
5161 options: File.HardLinkOptions,
5162) File.HardLinkError!void {
5163 _ = userdata;
5164 if (native_os != .linux) return error.OperationUnsupported;
5165
5166 var new_path_buffer: [posix.PATH_MAX]u8 = undefined;
5167 const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
5168
5169 const flags: u32 = if (!options.follow_symlinks)
5170 posix.AT.SYMLINK_NOFOLLOW | posix.AT.EMPTY_PATH
5171 else
5172 posix.AT.EMPTY_PATH;
5173
5174 return linkat(file.handle, "", new_dir.handle, new_sub_path_posix, flags);
5175}
5176
5177fn linkat(
5178 old_dir: posix.fd_t,
5179 old_path: [*:0]const u8,
5180 new_dir: posix.fd_t,
5181 new_path: [*:0]const u8,
5182 flags: u32,
5183) File.HardLinkError!void {
5184 const syscall: Syscall = try .start();
5185 while (true) {
5186 switch (posix.errno(posix.system.linkat(old_dir, old_path, new_dir, new_path, flags))) {
5187 .SUCCESS => return syscall.finish(),
5188 .INTR => {
5189 try syscall.checkCancel();
5190 continue;
5191 },
5192 .ACCES => return syscall.fail(error.AccessDenied),
5193 .DQUOT => return syscall.fail(error.DiskQuota),
5194 .EXIST => return syscall.fail(error.PathAlreadyExists),
5195 .IO => return syscall.fail(error.HardwareFailure),
5196 .LOOP => return syscall.fail(error.SymLinkLoop),
5197 .MLINK => return syscall.fail(error.LinkQuotaExceeded),
5198 .NAMETOOLONG => return syscall.fail(error.NameTooLong),
5199 .NOENT => return syscall.fail(error.FileNotFound),
5200 .NOMEM => return syscall.fail(error.SystemResources),
5201 .NOSPC => return syscall.fail(error.NoSpaceLeft),
5202 .NOTDIR => return syscall.fail(error.NotDir),
5203 .PERM => return syscall.fail(error.PermissionDenied),
5204 .ROFS => return syscall.fail(error.ReadOnlyFileSystem),
5205 .XDEV => return syscall.fail(error.NotSameFileSystem),
5206 .ILSEQ => return syscall.fail(error.BadPathName),
5207 .FAULT => |err| return syscall.errnoBug(err),
5208 .INVAL => |err| return syscall.errnoBug(err),
5209 else => |err| return syscall.unexpectedErrno(err),
5210 }
5211 }
5212}
5213
49885214const dirDeleteFile = switch (native_os) {
49895215 .windows => dirDeleteFileWindows,
49905216 .wasi => dirDeleteFileWasi,
......@@ -7325,7 +7551,6 @@ fn dirOpenDirWasi(
73257551 .NOMEM => return error.SystemResources,
73267552 .NOTDIR => return error.NotDir,
73277553 .PERM => return error.PermissionDenied,
7328 .BUSY => return error.DeviceBusy,
73297554 .NOTCAPABLE => return error.AccessDenied,
73307555 .ILSEQ => return error.BadPathName,
73317556 else => |err| return posix.unexpectedErrno(err),
......@@ -7401,46 +7626,7 @@ fn dirHardLink(
74017626 const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer);
74027627
74037628 const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0;
7404
7405 const syscall: Syscall = try .start();
7406 while (true) {
7407 switch (posix.errno(posix.system.linkat(
7408 old_dir.handle,
7409 old_sub_path_posix,
7410 new_dir.handle,
7411 new_sub_path_posix,
7412 flags,
7413 ))) {
7414 .SUCCESS => return syscall.finish(),
7415 .INTR => {
7416 try syscall.checkCancel();
7417 continue;
7418 },
7419 else => |e| {
7420 syscall.finish();
7421 switch (e) {
7422 .ACCES => return error.AccessDenied,
7423 .DQUOT => return error.DiskQuota,
7424 .EXIST => return error.PathAlreadyExists,
7425 .FAULT => |err| return errnoBug(err),
7426 .IO => return error.HardwareFailure,
7427 .LOOP => return error.SymLinkLoop,
7428 .MLINK => return error.LinkQuotaExceeded,
7429 .NAMETOOLONG => return error.NameTooLong,
7430 .NOENT => return error.FileNotFound,
7431 .NOMEM => return error.SystemResources,
7432 .NOSPC => return error.NoSpaceLeft,
7433 .NOTDIR => return error.NotDir,
7434 .PERM => return error.PermissionDenied,
7435 .ROFS => return error.ReadOnlyFileSystem,
7436 .XDEV => return error.NotSameFileSystem,
7437 .INVAL => |err| return errnoBug(err),
7438 .ILSEQ => return error.BadPathName,
7439 else => |err| return posix.unexpectedErrno(err),
7440 }
7441 },
7442 }
7443 }
7629 return linkat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix, flags);
74447630}
74457631
74467632fn fileClose(userdata: ?*anyopaque, files: []const File) void {
......@@ -13831,7 +14017,6 @@ fn windowsCreateProcessPathExt(
1383114017 error.NetworkNotFound,
1383214018 error.NameTooLong,
1383314019 error.BadPathName,
13834 error.DeviceBusy,
1383514020 => return error.FileNotFound,
1383614021 };
1383714022 };
lib/std/c.zig+2
......@@ -8427,6 +8427,7 @@ pub const O = switch (native_os) {
84278427 CLOEXEC: bool = false,
84288428 SYNC: bool = false,
84298429 PATH: bool = false,
8430 /// This is typically invalid without also setting `DIRECTORY`.
84308431 TMPFILE: bool = false,
84318432 _: u9 = 0,
84328433 },
......@@ -8615,6 +8616,7 @@ pub const O = switch (native_os) {
86158616 _19: u1 = 0,
86168617 CLOEXEC: bool = false,
86178618 PATH: bool = false,
8619 /// This is typically invalid without also setting `DIRECTORY`.
86188620 TMPFILE: bool = false,
86198621 _: u9 = 0,
86208622 },
lib/std/fs/test.zig+4-5
......@@ -1652,11 +1652,10 @@ test "AtomicFile" {
16521652 ;
16531653
16541654 {
1655 var buffer: [100]u8 = undefined;
1656 var af = try ctx.dir.atomicFile(io, test_out_file, .{ .write_buffer = &buffer });
1657 defer af.deinit();
1658 try af.file_writer.interface.writeAll(test_content);
1659 try af.finish();
1655 var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = true });
1656 defer af.deinit(io);
1657 try af.file.writeStreamingAll(io, test_content);
1658 try af.replace(io);
16601659 }
16611660 const content = try ctx.dir.readFileAlloc(io, test_out_file, allocator, .limited(9999));
16621661 try expectEqualStrings(test_content, content);
lib/std/os/linux.zig+12-3
......@@ -324,6 +324,7 @@ pub const O = switch (native_arch) {
324324 CLOEXEC: bool = false,
325325 SYNC: bool = false,
326326 PATH: bool = false,
327 /// This is typically invalid without also setting `DIRECTORY`.
327328 TMPFILE: bool = false,
328329 _23: u9 = 0,
329330 },
......@@ -346,6 +347,7 @@ pub const O = switch (native_arch) {
346347 CLOEXEC: bool = false,
347348 SYNC: bool = false,
348349 PATH: bool = false,
350 /// This is typically invalid without also setting `DIRECTORY`.
349351 TMPFILE: bool = false,
350352 _23: u9 = 0,
351353 },
......@@ -368,6 +370,7 @@ pub const O = switch (native_arch) {
368370 CLOEXEC: bool = false,
369371 SYNC: bool = false,
370372 PATH: bool = false,
373 /// This is typically invalid without also setting `DIRECTORY`.
371374 TMPFILE: bool = false,
372375 _23: u9 = 0,
373376 },
......@@ -393,6 +396,7 @@ pub const O = switch (native_arch) {
393396 CLOEXEC: bool = false,
394397 SYNC: bool = false,
395398 PATH: bool = false,
399 /// This is typically invalid without also setting `DIRECTORY`.
396400 TMPFILE: bool = false,
397401 _27: u6 = 0,
398402 },
......@@ -417,6 +421,7 @@ pub const O = switch (native_arch) {
417421 CLOEXEC: bool = false,
418422 _20: u1 = 0,
419423 PATH: bool = false,
424 /// This is typically invalid without also setting `DIRECTORY`.
420425 TMPFILE: bool = false,
421426 _23: u9 = 0,
422427 },
......@@ -439,6 +444,7 @@ pub const O = switch (native_arch) {
439444 CLOEXEC: bool = false,
440445 SYNC: bool = false,
441446 PATH: bool = false,
447 /// This is typically invalid without also setting `DIRECTORY`.
442448 TMPFILE: bool = false,
443449 _23: u9 = 0,
444450 },
......@@ -459,13 +465,16 @@ pub const O = switch (native_arch) {
459465 NOFOLLOW: bool = false,
460466 NOATIME: bool = false,
461467 CLOEXEC: bool = false,
462 _20: u1 = 0,
468 /// This is typically invalid without also setting `TMPFILE1` and `DIRECTORY`.
469 TMPFILE0: bool = false,
463470 PATH: bool = false,
464 _22: u10 = 0,
471 _22: u4 = 0,
472 /// This is typically invalid without also setting `TMPFILE0` and `DIRECTORY`.
473 TMPFILE1: bool = false,
474 _27: u5 = 0,
465475
466476 // #define O_RSYNC 04010000
467477 // #define O_SYNC 04010000
468 // #define O_TMPFILE 020200000
469478 // #define O_NDELAY O_NONBLOCK
470479 },
471480 .m68k => packed struct(u32) {
lib/std/zig/system.zig-1
......@@ -793,7 +793,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
793793 var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) {
794794 error.NameTooLong => return error.Unexpected,
795795 error.BadPathName => return error.Unexpected,
796 error.DeviceBusy => return error.Unexpected,
797796 error.NetworkNotFound => return error.Unexpected, // Windows-only
798797
799798 error.FileNotFound => return error.GLibCNotFound,
src/Builtin.zig+4-4
......@@ -343,10 +343,10 @@ pub fn updateFileOnDisk(file: *File, comp: *Compilation) !void {
343343 }
344344
345345 // `make_path` matters because the dir hasn't actually been created yet.
346 var af = try root_dir.atomicFile(io, sub_path, .{ .make_path = true, .write_buffer = &.{} });
347 defer af.deinit();
348 try af.file_writer.interface.writeAll(file.source.?);
349 af.finish() catch |err| switch (err) {
346 var af = try root_dir.createFileAtomic(io, sub_path, .{ .make_path = true, .replace = true });
347 defer af.deinit(io);
348 try af.file.writeStreamingAll(io, file.source.?);
349 af.replace(io) catch |err| switch (err) {
350350 error.AccessDenied => switch (builtin.os.tag) {
351351 .windows => {
352352 // Very likely happened due to another process or thread
src/Compilation.zig+20-12
......@@ -3916,11 +3916,14 @@ pub fn saveState(comp: *Compilation) !void {
39163916
39173917 // Using an atomic file prevents a crash or power failure from corrupting
39183918 // the previous incremental compilation state.
3919 var af = try lf.emit.root_dir.handle.createFileAtomic(io, basename, .{ .replace = true });
3920 defer af.deinit(io);
3921
39193922 var write_buffer: [1024]u8 = undefined;
3920 var af = try lf.emit.root_dir.handle.atomicFile(io, basename, .{ .write_buffer = &write_buffer });
3921 defer af.deinit();
3922 try af.file_writer.interface.writeVecAll(bufs.items);
3923 try af.finish();
3923 var file_writer = af.file.writer(io, &write_buffer);
3924 try file_writer.interface.writeVecAll(bufs.items);
3925 try file_writer.interface.flush();
3926 try af.replace(io);
39243927}
39253928
39263929fn addBuf(list: *std.array_list.Managed([]const u8), buf: []const u8) void {
......@@ -5244,26 +5247,31 @@ fn processOneJob(
52445247 }
52455248}
52465249
5247fn createDepFile(comp: *Compilation, depfile: []const u8, binfile: Cache.Path) anyerror!void {
5250fn createDepFile(comp: *Compilation, dep_file: []const u8, bin_file: Cache.Path) anyerror!void {
52485251 const io = comp.io;
5249 var buf: [4096]u8 = undefined;
5250 var af = try Io.Dir.cwd().atomicFile(io, depfile, .{ .write_buffer = &buf });
5251 defer af.deinit();
52525252
5253 comp.writeDepFile(binfile, &af.file_writer.interface) catch return af.file_writer.err.?;
5253 var af = try Io.Dir.cwd().createFileAtomic(io, dep_file, .{ .replace = true });
5254 defer af.deinit(io);
52545255
5255 try af.finish();
5256 var buf: [4096]u8 = undefined;
5257 var file_writer = af.file.writer(io, &buf);
5258
5259 comp.writeDepFile(bin_file, &file_writer.interface) catch |err| switch (err) {
5260 error.WriteFailed => return file_writer.err.?,
5261 };
5262 try file_writer.flush();
5263 try af.replace(io);
52565264}
52575265
52585266fn writeDepFile(
52595267 comp: *Compilation,
5260 binfile: Cache.Path,
5268 bin_file: Cache.Path,
52615269 w: *std.Io.Writer,
52625270) std.Io.Writer.Error!void {
52635271 const prefixes = comp.cache_parent.prefixes();
52645272 const fsi = comp.file_system_inputs.?.items;
52655273
5266 try w.print("{f}:", .{binfile});
5274 try w.print("{f}:", .{bin_file});
52675275
52685276 {
52695277 var it = std.mem.splitScalar(u8, fsi, 0);
src/fmt.zig+4-4
......@@ -355,11 +355,11 @@ fn fmtPathFile(
355355 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
356356 fmt.any_error = true;
357357 } else {
358 var af = try dir.atomicFile(io, sub_path, .{ .permissions = stat.permissions, .write_buffer = &.{} });
359 defer af.deinit();
358 var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true });
359 defer af.deinit(io);
360360
361 try af.file_writer.interface.writeAll(fmt.out_buffer.written());
362 try af.finish();
361 try af.file.writeStreamingAll(io, fmt.out_buffer.written());
362 try af.replace(io);
363363 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
364364 }
365365}