| author | |
| committer | |
| log | 81bfd289745c661ff5b96b42470f47492091c389 |
| tree | ed723e79bc4f7a7c6569e9b757ea972229c0c831 |
| parent | 6ab1159e815fdc7ce11fa49235509a43da74f899 |
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, | ||
| 713 | 715 | ||
| 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 | } |
| 600 | 599 | ||
| 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); | ||
| 611 | 609 | ||
| 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; |
| 614 | 612 | ||
| 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 | } |
| 627 | 625 | ||
| ... | @@ -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 | } |
| 997 | 995 | ||
| 998 | pub const HardLinkOptions = struct { | 996 | pub const HardLinkOptions = File.HardLinkOptions; |
| 999 | follow_symlinks: bool = true, | ||
| 1000 | }; | ||
| 1001 | 997 | ||
| 1002 | pub const HardLinkError = error{ | 998 | pub 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; | ||
| 1019 | 999 | ||
| 1020 | pub fn hardLink( | 1000 | pub 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 |
| 1658 | pub const CopyFileOptions = struct { | 1633 | pub 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 | }; |
| 1662 | 1639 | ||
| 1663 | pub const CopyFileError = File.OpenError || File.StatError || | 1640 | pub 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}; |
| 1666 | 1643 | ||
| 1667 | /// Atomically creates a new file at `dest_path` within `dest_dir` with the | 1644 | /// 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 | 1645 | /// 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 and | 1649 | /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and |
| 1672 | /// readily available, there is a possibility of power loss or application | 1650 | /// 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 | }; |
| 1697 | 1675 | ||
| 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); |
| 1704 | 1682 | ||
| 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 | }; |
| 1709 | 1690 | ||
| 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 | } |
| 1712 | 1698 | ||
| 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( |
| 1730 | 1716 | ||
| 1731 | test copyFileAbsolute {} | 1717 | test copyFileAbsolute {} |
| 1732 | 1718 | ||
| 1733 | pub const AtomicFileOptions = struct { | 1719 | pub 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 | }; |
| 1738 | 1729 | ||
| 1739 | /// Directly access the `.file` field, and then call `File.Atomic.finish` to | 1730 | pub 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 whether | 1733 | NetworkNotFound, |
| 1743 | /// `File.Atomic.finish` succeeded. `dest_path` must remain valid until | 1734 | /// 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, |
| 1749 | pub 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 | else | 1744 | SymLinkLoop, |
| 1754 | try parent.openDir(io, dirname, .{}); | 1745 | ProcessFdQuotaExceeded, |
| 1755 | 1746 | 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. | ||
| 1771 | pub 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 | } |
| 1761 | 1779 | ||
| 1762 | pub const SetPermissionsError = File.SetPermissionsError; | 1780 | pub 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; |
| 282 | 282 | ||
| 283 | pub fn close(file: File, io: Io) void { | 283 | pub 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 | } |
| 710 | 710 | ||
| 711 | pub const HardLinkOptions = struct { | ||
| 712 | follow_symlinks: bool = true, | ||
| 713 | }; | ||
| 714 | |||
| 715 | pub 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 | |||
| 733 | pub 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 | |||
| 711 | test { | 743 | test { |
| 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; |
| 6 | const Dir = std.Io.Dir; | 6 | const Dir = std.Io.Dir; |
| 7 | const assert = std.debug.assert; | 7 | const assert = std.debug.assert; |
| 8 | 8 | ||
| 9 | file_writer: File.Writer, | 9 | file: File, |
| 10 | random_integer: u64, | 10 | file_basename_hex: u64, |
| 11 | dest_basename: []const u8, | ||
| 12 | file_open: bool, | 11 | file_open: bool, |
| 13 | file_exists: bool, | 12 | file_exists: bool, |
| 14 | close_dir_on_deinit: bool, | 13 | |
| 15 | dir: Dir, | 14 | dir: Dir, |
| 15 | close_dir_on_deinit: bool, | ||
| 16 | 16 | ||
| 17 | pub const InitError = File.OpenError; | 17 | dest_sub_path: []const u8, |
| 18 | 18 | ||
| 19 | /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. | 19 | pub const InitError = File.OpenError; |
| 20 | pub 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(). | ||
| 51 | pub fn deinit(af: *Atomic) void { | ||
| 52 | const io = af.file_writer.io; | ||
| 53 | 20 | ||
| 21 | /// To release all resources, always call `deinit`, even after a successful | ||
| 22 | /// `finish`. | ||
| 23 | pub 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 | } |
| 68 | 39 | ||
| 69 | pub const FlushError = File.Writer.Error; | 40 | pub const LinkError = Dir.HardLinkError; |
| 70 | 41 | ||
| 71 | pub 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.?, | 44 | pub 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 | } |
| 76 | 61 | ||
| 77 | pub const RenameIntoPlaceError = Dir.RenameError; | 62 | pub const ReplaceError = Dir.RenameError; |
| 78 | 63 | ||
| 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 file | 70 | /// On Windows, this function introduces a period of time where some file |
| 80 | /// system operations on the destination file will result in | 71 | /// system operations on the destination file will result in |
| 81 | /// `error.AccessDenied`, including rename operations (such as the one used in | 72 | /// `error.AccessDenied`, including rename operations (such as the one used in |
| 82 | /// this function). | 73 | /// this function). |
| 83 | pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void { | 74 | pub 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 | |||
| 96 | pub const FinishError = FlushError || RenameIntoPlaceError; | ||
| 97 | |||
| 98 | /// Combination of `flush` followed by `renameIntoPlace`. | ||
| 99 | pub 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. | ||
| 278 | pub 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 | } |
| 3415 | 3417 | ||
| 3418 | fn 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 | |||
| 3537 | fn 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 | |||
| 3416 | const dirOpenFile = switch (native_os) { | 3575 | const 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-only | 796 | error.NetworkNotFound => return error.Unexpected, // Windows-only |
| 798 | 797 | ||
| 799 | error.FileNotFound => return error.GLibCNotFound, | 798 | error.FileNotFound => return error.GLibCNotFound, |