diff --git a/CMakeLists.txt b/CMakeLists.txt index 8aaeb288ed..4481dfb7f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/lib/std/fmt/errol/lookup.zig" "${CMAKE_SOURCE_DIR}/lib/std/fmt/parse_float.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs.zig" + "${CMAKE_SOURCE_DIR}/lib/std/fs/AtomicFile.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/Dir.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/file.zig" "${CMAKE_SOURCE_DIR}/lib/std/fs/get_app_data_dir.zig" diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 1a4b42ef1b..1aad688395 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -7,11 +7,11 @@ const base64 = std.base64; const crypto = std.crypto; const Allocator = std.mem.Allocator; const assert = std.debug.assert; -const math = std.math; const is_darwin = builtin.os.tag.isDarwin(); pub const Dir = @import("fs/Dir.zig"); +pub const AtomicFile = @import("fs/AtomicFile.zig"); pub const has_executable_bit = switch (builtin.os.tag) { .windows, .wasi => false, @@ -150,85 +150,6 @@ pub fn copyFileAbsolute( return Dir.copyFile(my_cwd, source_path, my_cwd, dest_path, args); } -pub const AtomicFile = struct { - file: File, - // TODO either replace this with rand_buf or use []u16 on Windows - tmp_path_buf: [TMP_PATH_LEN:0]u8, - dest_basename: []const u8, - file_open: bool, - file_exists: bool, - close_dir_on_deinit: bool, - dir: Dir, - - pub const InitError = File.OpenError; - - const RANDOM_BYTES = 12; - const TMP_PATH_LEN = base64_encoder.calcSize(RANDOM_BYTES); - - /// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. - pub fn init( - dest_basename: []const u8, - mode: File.Mode, - dir: Dir, - close_dir_on_deinit: bool, - ) InitError!AtomicFile { - var rand_buf: [RANDOM_BYTES]u8 = undefined; - var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; - - while (true) { - crypto.random.bytes(rand_buf[0..]); - const tmp_path = base64_encoder.encode(&tmp_path_buf, &rand_buf); - tmp_path_buf[tmp_path.len] = 0; - - const file = dir.createFile( - tmp_path, - .{ .mode = mode, .exclusive = true }, - ) catch |err| switch (err) { - error.PathAlreadyExists => continue, - else => |e| return e, - }; - - return AtomicFile{ - .file = file, - .tmp_path_buf = tmp_path_buf, - .dest_basename = dest_basename, - .file_open = true, - .file_exists = true, - .close_dir_on_deinit = close_dir_on_deinit, - .dir = dir, - }; - } - } - - /// Always call deinit, even after a successful finish(). - pub fn deinit(self: *AtomicFile) void { - if (self.file_open) { - self.file.close(); - self.file_open = false; - } - if (self.file_exists) { - self.dir.deleteFile(&self.tmp_path_buf) catch {}; - self.file_exists = false; - } - if (self.close_dir_on_deinit) { - self.dir.close(); - } - self.* = undefined; - } - - pub const FinishError = std.os.RenameError; - - pub fn finish(self: *AtomicFile) FinishError!void { - assert(self.file_exists); - if (self.file_open) { - self.file.close(); - self.file_open = false; - } - try os.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); - self.file_exists = false; - } -}; - /// Create a new directory, based on an absolute path. /// Asserts that the path is absolute. See `Dir.makeDir` for a function that operates /// on both absolute and relative paths. diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig new file mode 100644 index 0000000000..3e3ab92d08 --- /dev/null +++ b/lib/std/fs/AtomicFile.zig @@ -0,0 +1,84 @@ +file: File, +// TODO either replace this with rand_buf or use []u16 on Windows +tmp_path_buf: [TMP_PATH_LEN:0]u8, +dest_basename: []const u8, +file_open: bool, +file_exists: bool, +close_dir_on_deinit: bool, +dir: Dir, + +pub const InitError = File.OpenError; + +const RANDOM_BYTES = 12; +const TMP_PATH_LEN = fs.base64_encoder.calcSize(RANDOM_BYTES); + +/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. +pub fn init( + dest_basename: []const u8, + mode: File.Mode, + dir: Dir, + close_dir_on_deinit: bool, +) InitError!AtomicFile { + var rand_buf: [RANDOM_BYTES]u8 = undefined; + var tmp_path_buf: [TMP_PATH_LEN:0]u8 = undefined; + + while (true) { + std.crypto.random.bytes(rand_buf[0..]); + const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf); + tmp_path_buf[tmp_path.len] = 0; + + const file = dir.createFile( + tmp_path, + .{ .mode = mode, .exclusive = true }, + ) catch |err| switch (err) { + error.PathAlreadyExists => continue, + else => |e| return e, + }; + + return AtomicFile{ + .file = file, + .tmp_path_buf = tmp_path_buf, + .dest_basename = dest_basename, + .file_open = true, + .file_exists = true, + .close_dir_on_deinit = close_dir_on_deinit, + .dir = dir, + }; + } +} + +/// Always call deinit, even after a successful finish(). +pub fn deinit(self: *AtomicFile) void { + if (self.file_open) { + self.file.close(); + self.file_open = false; + } + if (self.file_exists) { + self.dir.deleteFile(&self.tmp_path_buf) catch {}; + self.file_exists = false; + } + if (self.close_dir_on_deinit) { + self.dir.close(); + } + self.* = undefined; +} + +pub const FinishError = posix.RenameError; + +pub fn finish(self: *AtomicFile) FinishError!void { + assert(self.file_exists); + if (self.file_open) { + self.file.close(); + self.file_open = false; + } + try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename); + self.file_exists = false; +} + +const AtomicFile = @This(); +const std = @import("../std.zig"); +const File = std.fs.File; +const Dir = std.fs.Dir; +const fs = std.fs; +const assert = std.debug.assert; +const posix = std.os;