authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 21:26:27-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-04 23:45:17-08:00
logbed0900c8c4b9eeb40dcb7fa39123f4f2427e9c7
treee4278384d9492b9c3343e93458c9202abb23506f
parent866989881927f256af1c495577007d715c6ae610

std.Io.Dir: rework atomic file


7 files changed, 336 insertions(+), 137 deletions(-)

lib/std/Io.zig+2
...@@ -667,6 +667,7 @@ pub const VTable = struct {...@@ -667,6 +667,7 @@ pub const VTable = struct {
667 dirStatFile: *const fn (?*anyopaque, Dir, []const u8, Dir.StatFileOptions) Dir.StatFileError!File.Stat,667 dirStatFile: *const fn (?*anyopaque, Dir, []const u8, Dir.StatFileOptions) Dir.StatFileError!File.Stat,
668 dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void,668 dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void,
669 dirCreateFile: *const fn (?*anyopaque, Dir, []const u8, File.CreateFlags) File.OpenError!File,669 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,
670 dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File,671 dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File,
671 dirClose: *const fn (?*anyopaque, []const Dir) void,672 dirClose: *const fn (?*anyopaque, []const Dir) void,
672 dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize,673 dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize,
...@@ -710,6 +711,7 @@ pub const VTable = struct {...@@ -710,6 +711,7 @@ pub const VTable = struct {
710 fileUnlock: *const fn (?*anyopaque, File) void,711 fileUnlock: *const fn (?*anyopaque, File) void,
711 fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,712 fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void,
712 fileRealPath: *const fn (?*anyopaque, File, out_buffer: []u8) File.RealPathError!usize,713 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
714 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,716 processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File,
715 processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize,717 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{...@@ -454,7 +454,6 @@ pub const OpenError = error{
454 SystemFdQuotaExceeded,454 SystemFdQuotaExceeded,
455 NoDevice,455 NoDevice,
456 SystemResources,456 SystemResources,
457 DeviceBusy,
458 /// On Windows, `\\server` or `\\server\share` was not found.457 /// On Windows, `\\server` or `\\server\share` was not found.
459 NetworkNotFound,458 NetworkNotFound,
460} || PathNameError || Io.Cancelable || Io.UnexpectedError;459} || PathNameError || Io.Cancelable || Io.UnexpectedError;
...@@ -598,30 +597,29 @@ pub fn updateFile(...@@ -598,30 +597,29 @@ pub fn updateFile(
598 }597 }
599 }598 }
600599
601 if (path.dirname(dest_path)) |dirname| {600 var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
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, .{
607 .permissions = actual_permissions,601 .permissions = actual_permissions,
608 .write_buffer = &buffer,602 .make_path = true,
603 .replace = true,
609 });604 });
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
612 var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size);610 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
615 _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {613 _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
616 error.ReadFailed => return src_reader.err.?,614 error.ReadFailed => return src_reader.err.?,
617 error.WriteFailed => return atomic_file.file_writer.err.?,615 error.WriteFailed => return file_writer.err.?,
618 };616 };
619 try atomic_file.flush();617 try file_writer.flush();
620 try atomic_file.file_writer.file.setTimestamps(io, .{618 try file_writer.file.setTimestamps(io, .{
621 .access_timestamp = .init(src_stat.atime),619 .access_timestamp = .init(src_stat.atime),
622 .modify_timestamp = .init(src_stat.mtime),620 .modify_timestamp = .init(src_stat.mtime),
623 });621 });
624 try atomic_file.renameIntoPlace();622 try atomic_file.replace(io);
625 return .stale;623 return .stale;
626}624}
627625
...@@ -995,27 +993,9 @@ pub fn renameAbsolute(old_path: []const u8, new_path: []const u8, io: Io) Rename...@@ -995,27 +993,9 @@ pub fn renameAbsolute(old_path: []const u8, new_path: []const u8, io: Io) Rename
995 return io.vtable.dirRename(io.userdata, my_cwd, old_path, my_cwd, new_path);993 return io.vtable.dirRename(io.userdata, my_cwd, old_path, my_cwd, new_path);
996}994}
997995
998pub const HardLinkOptions = struct {996pub const HardLinkOptions = File.HardLinkOptions;
999 follow_symlinks: bool = true,
1000};
1001997
1002pub const HardLinkError = error{998pub const HardLinkError = File.HardLinkError;
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;
1019999
1020pub fn hardLink(1000pub fn hardLink(
1021 old_dir: Dir,1001 old_dir: Dir,
...@@ -1251,7 +1231,6 @@ pub const DeleteTreeError = error{...@@ -1251,7 +1231,6 @@ pub const DeleteTreeError = error{
1251 ReadOnlyFileSystem,1231 ReadOnlyFileSystem,
1252 FileSystem,1232 FileSystem,
1253 FileBusy,1233 FileBusy,
1254 DeviceBusy,
1255 /// One of the path components was not a directory.1234 /// One of the path components was not a directory.
1256 /// This error is unreachable if `sub_path` does not contain a path separator.1235 /// This error is unreachable if `sub_path` does not contain a path separator.
1257 NotDir,1236 NotDir,
...@@ -1322,7 +1301,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {...@@ -1322,7 +1301,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
1322 error.Unexpected,1301 error.Unexpected,
1323 error.BadPathName,1302 error.BadPathName,
1324 error.NetworkNotFound,1303 error.NetworkNotFound,
1325 error.DeviceBusy,
1326 error.Canceled,1304 error.Canceled,
1327 => |e| return e,1305 => |e| return e,
1328 };1306 };
...@@ -1417,7 +1395,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {...@@ -1417,7 +1395,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void {
1417 error.Unexpected,1395 error.Unexpected,
1418 error.BadPathName,1396 error.BadPathName,
1419 error.NetworkNotFound,1397 error.NetworkNotFound,
1420 error.DeviceBusy,
1421 error.Canceled,1398 error.Canceled,
1422 => |e| return e,1399 => |e| return e,
1423 };1400 };
...@@ -1522,7 +1499,6 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8,...@@ -1522,7 +1499,6 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8,
1522 error.Unexpected,1499 error.Unexpected,
1523 error.BadPathName,1500 error.BadPathName,
1524 error.NetworkNotFound,1501 error.NetworkNotFound,
1525 error.DeviceBusy,
1526 error.Canceled,1502 error.Canceled,
1527 => |e| return e,1503 => |e| return e,
1528 };1504 };
...@@ -1619,7 +1595,6 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin...@@ -1619,7 +1595,6 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
1619 error.SystemResources,1595 error.SystemResources,
1620 error.Unexpected,1596 error.Unexpected,
1621 error.BadPathName,1597 error.BadPathName,
1622 error.DeviceBusy,
1623 error.NetworkNotFound,1598 error.NetworkNotFound,
1624 error.Canceled,1599 error.Canceled,
1625 => |e| return e,1600 => |e| return e,
...@@ -1658,15 +1633,18 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin...@@ -1658,15 +1633,18 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin
1658pub const CopyFileOptions = struct {1633pub const CopyFileOptions = struct {
1659 /// When this is `null` the permissions are copied from the source file.1634 /// When this is `null` the permissions are copied from the source file.
1660 permissions: ?File.Permissions = null,1635 permissions: ?File.Permissions = null,
1636 make_path: bool = false,
1637 replace: bool = true,
1661};1638};
16621639
1663pub const CopyFileError = File.OpenError || File.StatError ||1640pub const CopyFileError = File.OpenError || File.StatError ||
1664 File.Atomic.InitError || File.Atomic.FinishError ||1641 CreateFileAtomicError || File.Atomic.ReplaceError || File.Atomic.LinkError ||
1665 File.Reader.Error || File.Writer.Error || error{InvalidFileName};1642 File.Reader.Error || File.Writer.Error || error{InvalidFileName};
16661643
1667/// Atomically creates a new file at `dest_path` within `dest_dir` with the1644/// Atomically creates a new file at `dest_path` within `dest_dir` with the
1668/// same contents as `source_path` within `source_dir`, overwriting any already1645/// same contents as `source_path` within `source_dir`.
1669/// existing file.1646///
1647/// Whether to overwrite the existing file is determined by `options`.
1670///1648///
1671/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and1649/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and
1672/// readily available, there is a possibility of power loss or application1650/// readily available, there is a possibility of power loss or application
...@@ -1695,19 +1673,27 @@ pub fn copyFile(...@@ -1695,19 +1673,27 @@ pub fn copyFile(
1695 break :blk st.permissions;1673 break :blk st.permissions;
1696 };1674 };
16971675
1698 var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.1676 var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
1699 var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
1700 .permissions = permissions,1677 .permissions = permissions,
1701 .write_buffer = &buffer,1678 .make_path = options.make_path,
1679 .replace = options.replace,
1702 });1680 });
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) {
1706 error.ReadFailed => return file_reader.err.?,1687 error.ReadFailed => return file_reader.err.?,
1707 error.WriteFailed => return atomic_file.file_writer.err.?,1688 error.WriteFailed => return file_writer.err.?,
1708 };1689 };
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 }
1711}1697}
17121698
1713/// Same as `copyFile`, except asserts that both `source_path` and `dest_path`1699/// Same as `copyFile`, except asserts that both `source_path` and `dest_path`
...@@ -1730,33 +1716,65 @@ pub fn copyFileAbsolute(...@@ -1730,33 +1716,65 @@ pub fn copyFileAbsolute(
17301716
1731test copyFileAbsolute {}1717test copyFileAbsolute {}
17321718
1733pub const AtomicFileOptions = struct {1719pub const CreateFileAtomicOptions = struct {
1734 permissions: File.Permissions = .default_file,1720 permissions: File.Permissions = .default_file,
1735 make_path: bool = false,1721 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,
1737};1728};
17381729
1739/// Directly access the `.file` field, and then call `File.Atomic.finish` to1730pub const CreateFileAtomicError = error{
1740/// atomically replace `dest_path` with contents.1731 NoDevice,
1741///1732 /// On Windows, `\\server` or `\\server\share` was not found.
1742/// Always call `File.Atomic.deinit` to clean up, regardless of whether1733 NetworkNotFound,
1743/// `File.Atomic.finish` succeeded. `dest_path` must remain valid until1734 /// On Windows, antivirus software is enabled by default. It can be
1744/// `File.Atomic.deinit` is called.1735 /// disabled, but Windows Update sometimes ignores the user's preference
1745///1736 /// and re-enables it. When enabled, antivirus software on Windows
1746/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/).1737 /// intercepts file system operations and makes them significantly slower
1747/// On WASI, `dest_path` should be encoded as valid UTF-8.1738 /// in addition to possibly failing with this error code.
1748/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding.1739 AntivirusInterference,
1749pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic {1740 /// In WASI, this error may occur when the file descriptor does
1750 if (path.dirname(dest_path)) |dirname| {1741 /// not hold the required rights to open a new resource relative to it.
1751 const dir = if (options.make_path)1742 AccessDenied,
1752 try parent.createDirPathOpen(io, dirname, .{})1743 PermissionDenied,
1753 else1744 SymLinkLoop,
1754 try parent.openDir(io, dirname, .{});1745 ProcessFdQuotaExceeded,
17551746 SystemFdQuotaExceeded,
1756 return .init(io, path.basename(dest_path), options.permissions, dir, true, options.write_buffer);1747 /// Either:
1757 } else {1748 /// * One of the path components does not exist.
1758 return .init(io, dest_path, options.permissions, parent, false, options.write_buffer);1749 /// * Cwd was used, but cwd has been deleted.
1759 }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);
1760}1778}
17611779
1762pub const SetPermissionsError = File.SetPermissionsError;1780pub const SetPermissionsError = File.SetPermissionsError;
lib/std/Io/File.zig+33-1
...@@ -278,7 +278,7 @@ pub const OpenError = error{...@@ -278,7 +278,7 @@ pub const OpenError = error{
278 FileBusy,278 FileBusy,
279 /// Non-blocking was requested and the operation cannot return immediately.279 /// Non-blocking was requested and the operation cannot return immediately.
280 WouldBlock,280 WouldBlock,
281} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;281} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError;
282282
283pub fn close(file: File, io: Io) void {283pub fn close(file: File, io: Io) void {
284 return io.vtable.fileClose(io.userdata, (&file)[0..1]);284 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 {...@@ -708,6 +708,38 @@ pub fn realPath(file: File, io: Io, out_buffer: []u8) RealPathError!usize {
708 return io.vtable.fileRealPath(io.userdata, file, out_buffer);708 return io.vtable.fileRealPath(io.userdata, file, out_buffer);
709}709}
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
711test {743test {
712 _ = Reader;744 _ = Reader;
713 _ = Writer;745 _ = Writer;
lib/std/Io/File/Atomic.zig+43-62
...@@ -6,97 +6,78 @@ const File = std.Io.File;...@@ -6,97 +6,78 @@ const File = std.Io.File;
6const Dir = std.Io.Dir;6const Dir = std.Io.Dir;
7const assert = std.debug.assert;7const assert = std.debug.assert;
88
9file_writer: File.Writer,9file: File,
10random_integer: u64,10file_basename_hex: u64,
11dest_basename: []const u8,
12file_open: bool,11file_open: bool,
13file_exists: bool,12file_exists: bool,
14close_dir_on_deinit: bool,13
15dir: Dir,14dir: 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.19pub const InitError = File.OpenError;
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;
5320
21/// To release all resources, always call `deinit`, even after a successful
22/// `finish`.
23pub fn deinit(af: *Atomic, io: Io) void {
54 if (af.file_open) {24 if (af.file_open) {
55 af.file_writer.file.close(io);25 af.file.close(io);
56 af.file_open = false;26 af.file_open = false;
57 }27 }
58 if (af.file_exists) {28 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);
60 af.dir.deleteFile(io, &tmp_sub_path) catch {};30 af.dir.deleteFile(io, &tmp_sub_path) catch {};
61 af.file_exists = false;31 af.file_exists = false;
62 }32 }
63 if (af.close_dir_on_deinit) {33 if (af.close_dir_on_deinit) {
64 af.dir.close(io);34 af.dir.close(io);
35 af.close_dir_on_deinit = false;
65 }36 }
66 af.* = undefined;37 af.* = undefined;
67}38}
6839
69pub const FlushError = File.Writer.Error;40pub const LinkError = Dir.HardLinkError;
7041
71pub fn flush(af: *Atomic) FlushError!void {42/// Atomically materializes the file into place, failing with
72 af.file_writer.interface.flush() catch |err| switch (err) {43/// `error.PathAlreadyExists` if something already exists there.
73 error.WriteFailed => return af.file_writer.err.?,44pub fn link(af: *Atomic, io: Io) LinkError!void {
74 };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 }
75}60}
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///
79/// On Windows, this function introduces a period of time where some file70/// On Windows, this function introduces a period of time where some file
80/// system operations on the destination file will result in71/// system operations on the destination file will result in
81/// `error.AccessDenied`, including rename operations (such as the one used in72/// `error.AccessDenied`, including rename operations (such as the one used in
82/// this function).73/// this function).
83pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void {74pub fn replace(af: *Atomic, io: Io) ReplaceError!void {
84 const io = af.file_writer.io;75 assert(af.file_exists); // Wrong value for `CreateFileAtomicOptions.replace`.
85
86 assert(af.file_exists);
87 if (af.file_open) {76 if (af.file_open) {
88 af.file_writer.file.close(io);77 af.file.close(io);
89 af.file_open = false;78 af.file_open = false;
90 }79 }
91 const tmp_sub_path = std.fmt.hex(af.random_integer);80 const tmp_sub_path = std.fmt.hex(af.file_basename_hex);
92 try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io);81 try af.dir.rename(&tmp_sub_path, af.dir, af.dest_sub_path, io);
93 af.file_exists = false;82 af.file_exists = false;
94}83}
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 {...@@ -272,3 +272,11 @@ pub fn end(w: *Writer) EndError!void {
272 => {},272 => {},
273 }273 }
274}274}
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+160-1
...@@ -1403,6 +1403,7 @@ pub fn io(t: *Threaded) Io {...@@ -1403,6 +1403,7 @@ pub fn io(t: *Threaded) Io {
1403 .dirStatFile = dirStatFile,1403 .dirStatFile = dirStatFile,
1404 .dirAccess = dirAccess,1404 .dirAccess = dirAccess,
1405 .dirCreateFile = dirCreateFile,1405 .dirCreateFile = dirCreateFile,
1406 .dirCreateFileAtomic = dirCreateFileAtomic,
1406 .dirOpenFile = dirOpenFile,1407 .dirOpenFile = dirOpenFile,
1407 .dirOpenDir = dirOpenDir,1408 .dirOpenDir = dirOpenDir,
1408 .dirClose = dirClose,1409 .dirClose = dirClose,
...@@ -1549,6 +1550,7 @@ pub fn ioBasic(t: *Threaded) Io {...@@ -1549,6 +1550,7 @@ pub fn ioBasic(t: *Threaded) Io {
1549 .dirStatFile = dirStatFile,1550 .dirStatFile = dirStatFile,
1550 .dirAccess = dirAccess,1551 .dirAccess = dirAccess,
1551 .dirCreateFile = dirCreateFile,1552 .dirCreateFile = dirCreateFile,
1553 .dirCreateFileAtomic = dirCreateFileAtomic,
1552 .dirOpenFile = dirOpenFile,1554 .dirOpenFile = dirOpenFile,
1553 .dirOpenDir = dirOpenDir,1555 .dirOpenDir = dirOpenDir,
1554 .dirClose = dirClose,1556 .dirClose = dirClose,
...@@ -3413,6 +3415,163 @@ fn dirCreateFileWasi(...@@ -3413,6 +3415,163 @@ fn dirCreateFileWasi(
3413 }3415 }
3414}3416}
34153417
3418fn dirCreateFileAtomic(
3419 userdata: ?*anyopaque,
3420 dir: Dir,
3421 dest_path: []const u8,
3422 options: Dir.CreateFileAtomicOptions,
3423) Dir.CreateFileAtomicError!File.Atomic {
3424 const t: *Threaded = @ptrCast(@alignCast(userdata));
3425 const t_io = ioBasic(t);
3426
3427 // Linux has O_TMPFILE, but linkat() does not support AT_REPLACE, so it's
3428 // useless when we have to make up a bogus path name to do the rename()
3429 // anyway.
3430 if (native_os == .linux and !options.replace) tmpfile: {
3431 const dest_dirname = Dir.path.dirname(dest_path);
3432 if (dest_dirname) |dirname| {
3433 // This has a nice side effect of preemptively triggering EISDIR or
3434 // ENOENT, avoiding the ambiguity below.
3435 dir.createDirPath(t_io, dirname) catch |err| switch (err) {
3436 // None of these make sense in this context.
3437 error.IsDir,
3438 error.Streaming,
3439 error.DiskQuota,
3440 error.PathAlreadyExists,
3441 error.LinkQuotaExceeded,
3442 error.SharingViolation,
3443 error.PipeBusy,
3444 error.FileTooBig,
3445 error.DeviceBusy,
3446 error.FileLocksUnsupported,
3447 error.FileBusy,
3448 => return error.Unexpected,
3449
3450 else => |e| return e,
3451 };
3452 }
3453
3454 var path_buffer: [posix.PATH_MAX]u8 = undefined;
3455 const sub_path_posix = try pathToPosix(dest_dirname orelse ".", &path_buffer);
3456
3457 const flags: posix.O = .{
3458 .ACCMODE = .RDWR,
3459 .TMPFILE = true,
3460 .CLOEXEC = true,
3461 };
3462
3463 const syscall: Syscall = try .start();
3464 while (true) {
3465 const rc = openat_sym(dir.handle, sub_path_posix, flags, options.permissions.toMode());
3466 switch (posix.errno(rc)) {
3467 .SUCCESS => {
3468 syscall.finish();
3469 return .{
3470 .file = .{ .handle = @intCast(rc) },
3471 .file_basename_hex = 0,
3472 .dest_sub_path = dest_path,
3473 .file_open = true,
3474 .file_exists = false,
3475 .close_dir_on_deinit = false,
3476 .dir = dir,
3477 };
3478 },
3479 .INTR => {
3480 try syscall.checkCancel();
3481 continue;
3482 },
3483 .ISDIR, .NOENT => {
3484 // Ambiguous error code. It might mean the file system
3485 // does not support O_TMPFILE. Therefore, we must fall
3486 // back to not using O_TMPFILE.
3487 syscall.finish();
3488 break :tmpfile;
3489 },
3490 .INVAL => return syscall.fail(error.BadPathName),
3491 .ACCES => return syscall.fail(error.AccessDenied),
3492 .LOOP => return syscall.fail(error.SymLinkLoop),
3493 .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded),
3494 .NAMETOOLONG => return syscall.fail(error.NameTooLong),
3495 .NFILE => return syscall.fail(error.SystemFdQuotaExceeded),
3496 .NODEV => return syscall.fail(error.NoDevice),
3497 .NOMEM => return syscall.fail(error.SystemResources),
3498 .NOSPC => return syscall.fail(error.NoSpaceLeft),
3499 .NOTDIR => return syscall.fail(error.NotDir),
3500 .PERM => return syscall.fail(error.PermissionDenied),
3501 .AGAIN => return syscall.fail(error.WouldBlock),
3502 .NXIO => return syscall.fail(error.NoDevice),
3503 .ILSEQ => return syscall.fail(error.BadPathName),
3504 else => |err| return syscall.unexpectedErrno(err),
3505 }
3506 }
3507 }
3508
3509 if (Dir.path.dirname(dest_path)) |dirname| {
3510 const new_dir = if (options.make_path)
3511 dir.createDirPathOpen(t_io, dirname, .{}) catch |err| switch (err) {
3512 // None of these make sense in this context.
3513 error.IsDir,
3514 error.Streaming,
3515 error.DiskQuota,
3516 error.PathAlreadyExists,
3517 error.LinkQuotaExceeded,
3518 error.SharingViolation,
3519 error.PipeBusy,
3520 error.FileTooBig,
3521 error.FileLocksUnsupported,
3522 error.FileBusy,
3523 error.DeviceBusy,
3524 => return error.Unexpected,
3525
3526 else => |e| return e,
3527 }
3528 else
3529 try dir.openDir(t_io, dirname, .{});
3530
3531 return atomicFileInit(t_io, Dir.path.basename(dest_path), options.permissions, new_dir, true);
3532 }
3533
3534 return atomicFileInit(t_io, dest_path, options.permissions, dir, false);
3535}
3536
3537fn atomicFileInit(
3538 t_io: Io,
3539 dest_basename: []const u8,
3540 permissions: File.Permissions,
3541 dir: Dir,
3542 close_dir_on_deinit: bool,
3543) Dir.CreateFileAtomicError!File.Atomic {
3544 while (true) {
3545 const random_integer = std.crypto.random.int(u64);
3546 const tmp_sub_path = std.fmt.hex(random_integer);
3547 const file = dir.createFile(t_io, &tmp_sub_path, .{
3548 .permissions = permissions,
3549 .exclusive = true,
3550 }) catch |err| switch (err) {
3551 error.PathAlreadyExists => continue,
3552 error.DeviceBusy => continue,
3553 error.FileBusy => continue,
3554 error.SharingViolation => continue,
3555
3556 error.IsDir => return error.Unexpected, // No path components.
3557 error.FileTooBig => return error.Unexpected, // Creating, not opening.
3558 error.FileLocksUnsupported => return error.Unexpected, // Not asking for locks.
3559 error.PipeBusy => return error.Unexpected, // Not opening a pipe.
3560
3561 else => |e| return e,
3562 };
3563 return .{
3564 .file = file,
3565 .file_basename_hex = random_integer,
3566 .dest_sub_path = dest_basename,
3567 .file_open = true,
3568 .file_exists = true,
3569 .close_dir_on_deinit = close_dir_on_deinit,
3570 .dir = dir,
3571 };
3572 }
3573}
3574
3416const dirOpenFile = switch (native_os) {3575const dirOpenFile = switch (native_os) {
3417 .windows => dirOpenFileWindows,3576 .windows => dirOpenFileWindows,
3418 .wasi => dirOpenFileWasi,3577 .wasi => dirOpenFileWasi,
...@@ -3925,7 +4084,7 @@ fn dirOpenDirPosix(...@@ -3925,7 +4084,7 @@ fn dirOpenDirPosix(
3925 .NOMEM => return error.SystemResources,4084 .NOMEM => return error.SystemResources,
3926 .NOTDIR => return error.NotDir,4085 .NOTDIR => return error.NotDir,
3927 .PERM => return error.PermissionDenied,4086 .PERM => return error.PermissionDenied,
3928 .BUSY => return error.DeviceBusy,4087 .BUSY => |err| return errnoBug(err), // O_EXCL not passed
3929 .NXIO => return error.NoDevice,4088 .NXIO => return error.NoDevice,
3930 .ILSEQ => return error.BadPathName,4089 .ILSEQ => return error.BadPathName,
3931 else => |err| return posix.unexpectedErrno(err),4090 else => |err| return posix.unexpectedErrno(err),
lib/std/zig/system.zig-1
...@@ -793,7 +793,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {...@@ -793,7 +793,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion {
793 var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) {793 var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) {
794 error.NameTooLong => return error.Unexpected,794 error.NameTooLong => return error.Unexpected,
795 error.BadPathName => return error.Unexpected,795 error.BadPathName => return error.Unexpected,
796 error.DeviceBusy => return error.Unexpected,
797 error.NetworkNotFound => return error.Unexpected, // Windows-only796 error.NetworkNotFound => return error.Unexpected, // Windows-only
798797
799 error.FileNotFound => return error.GLibCNotFound,798 error.FileNotFound => return error.GLibCNotFound,