diff --git a/sp.h b/sp.h index ce01011e..c4f94e27 100644 --- a/sp.h +++ b/sp.h @@ -1452,10 +1452,12 @@ SP_API sp_err_t sp_sys_unlink(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_rename(sp_sys_fd_t from, const c8* pfrom, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); SP_API sp_err_t sp_sys_link(sp_sys_fd_t from, const c8* pfrom, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); SP_API sp_err_t sp_sys_symlink(const c8* from, u32 lf, sp_sys_fd_t to, const c8* pto, u32 lt); +SP_API sp_err_t sp_sys_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); SP_API sp_err_t sp_sys_get_path_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_link_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_file_metadata(sp_sys_fd_t fd, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_chmod(sp_sys_fd_t fd, const c8* path, u32 len, const sp_sys_file_meta_t* st); +SP_API sp_err_t sp_sys_set_times(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_timespec_t atime, sp_sys_timespec_t mtime); SP_API sp_err_t sp_sys_clock_gettime(s32 clockid, sp_sys_timespec_t* ts); SP_API sp_err_t sp_sys_nanosleep(const sp_sys_timespec_t* req, sp_sys_timespec_t* rem); SP_API bool sp_sys_futex_wait(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); @@ -1511,7 +1513,9 @@ SP_API sp_err_t sp_sys_rename_s(sp_sys_fd_t from_fd, sp_str_t from, sp_sys_fd SP_API sp_err_t sp_sys_chdir_s(sp_str_t path); SP_API sp_err_t sp_sys_link_s(sp_sys_fd_t from_fd, sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias); SP_API sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias); +SP_API sp_err_t sp_sys_readlink_s(sp_sys_fd_t fd, sp_str_t path, c8* buf, u64 size, sp_str_t* target); SP_API sp_err_t sp_sys_chmod_s(sp_sys_fd_t fd, sp_str_t path, const sp_sys_file_meta_t* st); +SP_API sp_err_t sp_sys_set_times_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_timespec_t atime, sp_sys_timespec_t mtime); SP_API s64 sp_sys_canonicalize_path_s(sp_str_t path, c8* buf, u64 size); SP_API sp_err_t sp_sys_dir_from_fd(sp_sys_fd_t fd, sp_sys_dir_t* out); SP_API sp_err_t sp_sys_dir_read(sp_sys_dir_t* dir, sp_mem_buffer_t* buf); @@ -1545,10 +1549,12 @@ typedef struct { sp_err_t (*rename)(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_sys_fd_t to_fd, const c8* to, u32 to_len); sp_err_t (*link)(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); sp_err_t (*symlink)(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); + sp_err_t (*readlink)(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); sp_err_t (*get_path_metadata)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); sp_err_t (*get_link_metadata)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); sp_err_t (*get_file_metadata)(sp_sys_fd_t fd, sp_sys_file_meta_t* st); sp_err_t (*chmod)(sp_sys_fd_t fd, const c8* path, u32 len, const sp_sys_file_meta_t* st); + sp_err_t (*set_times)(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_timespec_t atime, sp_sys_timespec_t mtime); sp_err_t (*clock_gettime)(s32 clockid, sp_sys_timespec_t* ts); sp_err_t (*nanosleep)(const sp_sys_timespec_t* req, sp_sys_timespec_t* rem); bool (*futex_wait)(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); @@ -1618,10 +1624,12 @@ SP_API sp_err_t sp_sys_unlink_p(sp_sys_fd_t fd, const c8* path, u32 len); SP_API sp_err_t sp_sys_rename_p(sp_sys_fd_t from_fd, const c8* from, u32 from_len, sp_sys_fd_t to_fd, const c8* to, u32 to_len); SP_API sp_err_t sp_sys_link_p(sp_sys_fd_t from_fd, const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); SP_API sp_err_t sp_sys_symlink_p(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, const c8* alias, u32 alias_len); +SP_API sp_err_t sp_sys_readlink_p(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len); SP_API sp_err_t sp_sys_get_path_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_link_metadata_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_get_file_metadata_p(sp_sys_fd_t fd, sp_sys_file_meta_t* st); SP_API sp_err_t sp_sys_chmod_p(sp_sys_fd_t fd, const c8* path, u32 len, const sp_sys_file_meta_t* st); +SP_API sp_err_t sp_sys_set_times_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_timespec_t atime, sp_sys_timespec_t mtime); SP_API sp_err_t sp_sys_clock_gettime_p(s32 clockid, sp_sys_timespec_t* ts); SP_API sp_err_t sp_sys_nanosleep_p(const sp_sys_timespec_t* req, sp_sys_timespec_t* rem); SP_API bool sp_sys_futex_wait_p(u32* addr, u32 expected, const sp_sys_timespec_t* timeout); @@ -2998,9 +3006,13 @@ SP_API void sp_tm_epoch_to_iso8601_w(sp_io_writer_t* io, sp_tm_epoc typedef enum { SP_FS_LINK_HARD, SP_FS_LINK_SYMBOLIC, - SP_FS_LINK_COPY, } sp_fs_link_kind_t; +typedef enum { + SP_FS_ATOMIC_REPLACE, + SP_FS_ATOMIC_EXCLUSIVE, +} sp_fs_atomic_mode_t; + typedef enum { SP_OS_NO_FOLLOW_SYMLINK, SP_OS_FOLLOW_SYMLINK, @@ -3086,9 +3098,9 @@ SP_API sp_err_t sp_fs_create_hard_link(sp_str_t target, sp_str_t lin SP_API sp_err_t sp_fs_create_sym_link(sp_str_t target, sp_str_t link_path); SP_API sp_err_t sp_fs_link(sp_str_t from, sp_str_t to, sp_fs_link_kind_t kind); SP_API sp_err_t sp_fs_copy(sp_str_t from, sp_str_t to); -SP_API sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to); -SP_API sp_err_t sp_fs_copy_dir(sp_str_t from, sp_str_t to); -SP_API sp_err_t sp_fs_copy_glob(sp_str_t from, sp_str_t glob, sp_str_t to); +SP_API sp_err_t sp_fs_copy_into(sp_str_t from, sp_str_t dir); +SP_API sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode); +SP_API sp_err_t sp_fs_copy_tree(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode); SP_API sp_err_t sp_fs_dir_open(sp_fs_dir_t* it, sp_sys_fd_t fd, sp_str_t path, sp_mem_slice_t buf); SP_API sp_err_t sp_fs_dir_next(sp_fs_dir_t* it, sp_fs_dir_entry_t* out); SP_API sp_err_t sp_fs_dir_close(sp_fs_dir_t* it); @@ -4507,6 +4519,7 @@ typedef struct { sp_sys_fd_t file; u64 pos; u64 size; + bool size_known; sp_io_close_mode_t close_mode; } sp_io_file_reader_t; @@ -4533,6 +4546,7 @@ struct sp_io_file_writer { sp_sys_fd_t fd; u64 pos; u64 size; + bool size_known; sp_io_close_mode_t close_mode; }; @@ -4676,6 +4690,25 @@ SP_API const c8* sp_io_dyn_mem_writer_as_cstr(sp_io_dyn_mem_writer_t* w); SP_API sp_io_writer_t* sp_io_get_std_out(); SP_API sp_io_writer_t* sp_io_get_std_err(); +typedef struct { + sp_sys_fd_t dir; + sp_str_t path; + sp_str_t temp; + c8 temp_buf [SP_PATH_MAX]; + sp_io_file_writer_t writer; +} sp_fs_atomic_t; + +SP_API sp_err_t sp_fs_atomic_open(sp_fs_atomic_t* af, sp_str_t path); +SP_API sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_str_t path); +SP_API sp_err_t sp_fs_atomic_open_staged(sp_fs_atomic_t* af, sp_str_t path, sp_str_t staging); +SP_API sp_io_writer_t* sp_fs_atomic_writer(sp_fs_atomic_t* af); +SP_API sp_err_t sp_fs_atomic_commit(sp_fs_atomic_t* af, sp_fs_atomic_mode_t mode); +SP_API sp_err_t sp_fs_atomic_abort(sp_fs_atomic_t* af); +SP_API sp_err_t sp_fs_write_atomic(sp_str_t path, sp_str_t str); +SP_API sp_err_t sp_fs_write_atomic_slice(sp_str_t path, sp_mem_slice_t slice); +SP_API sp_err_t sp_fs_write_atomic_slice_staged(sp_str_t path, sp_str_t staging, sp_mem_slice_t slice); +SP_API sp_err_t sp_fs_write_atomic_cstr(sp_str_t path, const c8* str); + // ███████████ ███████████ ███████ █████████ ██████████ █████████ █████████ // ░░███░░░░░███░░███░░░░░███ ███░░░░░███ ███░░░░░███░░███░░░░░█ ███░░░░░███ ███░░░░░███ @@ -5013,7 +5046,6 @@ SP_IMP BOOL CALLBACK sp_tls_once_trampoline(PINIT_ONCE once, PVOID param, PVOI // @fs SP_IMP sp_fs_kind_t sp_fs_get_link_kind(sp_str_t path); -SP_IMP sp_err_t sp_fs_copy_file_meta(sp_str_t from, sp_str_t to, const sp_sys_file_meta_t* st); // @tls SP_PRIVATE void sp_tls_new(sp_tls_key_t* key, sp_tls_deinit_fn_t fn); @@ -5459,6 +5491,7 @@ SP_PRIVATE void sp_rt_init(void); #define SP_NT_STATUS_INVALID_HANDLE ((sp_nt_status_t)0xC0000008) #define SP_NT_STATUS_NOT_SAME_DEVICE ((sp_nt_status_t)0xC00000D4) #define SP_NT_STATUS_INVALID_DEVICE_REQUEST ((sp_nt_status_t)0xC0000010) +#define SP_NT_STATUS_NOT_A_REPARSE_POINT ((sp_nt_status_t)0xC0000275) #define SP_NT_STATUS_NO_SUCH_FILE ((sp_nt_status_t)0xC000000F) #define SP_NT_STATUS_NO_MORE_FILES ((sp_nt_status_t)0x80000006) #define SP_NT_STATUS_PENDING ((sp_nt_status_t)0x00000103) @@ -5489,6 +5522,7 @@ SP_PRIVATE void sp_rt_init(void); #define SP_NT_FILE_RENAME_IGNORE_READONLY_ATTRIBUTE 0x00000040 #define SP_NT_FSCTL_SET_REPARSE_POINT 0x000900A4 +#define SP_NT_FSCTL_GET_REPARSE_POINT 0x000900A8 #define SP_NT_IO_REPARSE_TAG_SYMLINK 0xA000000C #define SP_NT_SYMLINK_FLAG_RELATIVE 0x00000001 #define SP_NT_MAX_REPARSE_DATA 16384 @@ -5632,10 +5666,12 @@ const sp_sys_vtable_t sp_sys_vtable_platform = { .rename = sp_sys_rename_p, .link = sp_sys_link_p, .symlink = sp_sys_symlink_p, + .readlink = sp_sys_readlink_p, .get_path_metadata = sp_sys_get_path_metadata_p, .get_link_metadata = sp_sys_get_link_metadata_p, .get_file_metadata = sp_sys_get_file_metadata_p, .chmod = sp_sys_chmod_p, + .set_times = sp_sys_set_times_p, .clock_gettime = sp_sys_clock_gettime_p, .nanosleep = sp_sys_nanosleep_p, .futex_wait = sp_sys_futex_wait_p, @@ -5849,6 +5885,10 @@ sp_err_t sp_sys_symlink(const c8* existing, u32 existing_len, sp_sys_fd_t to_fd, return (sp_rt.vt->symlink)(existing, existing_len, to_fd, alias, alias_len); } +sp_err_t sp_sys_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len) { + return (sp_rt.vt->readlink)(fd, path, len, buf, size, target_len); +} + sp_err_t sp_sys_get_path_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st) { return (sp_rt.vt->get_path_metadata)(fd, path, len, st); } @@ -5865,6 +5905,10 @@ sp_err_t sp_sys_chmod(sp_sys_fd_t fd, const c8* path, u32 len, const sp_sys_file return (sp_rt.vt->chmod)(fd, path, len, st); } +sp_err_t sp_sys_set_times(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_timespec_t atime, sp_sys_timespec_t mtime) { + return (sp_rt.vt->set_times)(fd, path, len, atime, mtime); +} + sp_err_t sp_sys_clock_gettime(s32 clockid, sp_sys_timespec_t* ts) { return (sp_rt.vt->clock_gettime)(clockid, ts); } @@ -6818,6 +6862,50 @@ SP_PRIVATE sp_nt_status_t sp_sys_nt_from_dos_wtf16(const u16* dos, u32 len, u16* return SP_NT_STATUS_SUCCESS; } +// The inverse. Take: +// - Paths in the NT namespace (\??\), like in a reparse point +// - Paths in the Win32 verbatim namespace (\\?\), like GetFinalPathNameByHandle +// +// And turn them into the DOS spelling that Win32 accepts: +// +// \??\C:\A\B C:\A\B +// \??\UNC\srv\share\A \\srv\share\A +// \??\Volume{...}\A \\?\Volume{...}\A +// anything else unchanged +// +// Works in place and returns a view +SP_PRIVATE sp_wide_str_t sp_sys_dos_from_nt_wtf16(u16* nt, u32 len) { + sp_wide_str_t out = { .data = nt, .len = len }; + + bool prefixed = len >= 4 && + nt[0] == '\\' && + (nt[1] == '\\' || nt[1] == '?') && + nt[2] == '?' && + nt[3] == '\\'; + if (!prefixed) return out; + + bool drive = len >= 6 && nt[5] == ':'; + bool unc = len >= 8 && + (nt[4] == 'U' || nt[4] == 'u') && + (nt[5] == 'N' || nt[5] == 'n') && + (nt[6] == 'C' || nt[6] == 'c') && + nt[7] == '\\'; + + if (drive) { + out.data = nt + 4; + out.len = len - 4; + } + else if (unc) { + nt[6] = '\\'; + out.data = nt + 6; + out.len = len - 6; + } + else { + nt[1] = '\\'; + } + return out; +} + typedef struct { sp_nt_unicode_string_t name; HANDLE root; @@ -6952,7 +7040,8 @@ SP_PRIVATE sp_err_t sp_sys_err_from_nt(sp_nt_status_t status) { switch (status) { case SP_NT_STATUS_OBJECT_NAME_NOT_FOUND: case SP_NT_STATUS_OBJECT_PATH_NOT_FOUND: return SP_ERR_SYS_NOT_FOUND; - case SP_NT_STATUS_OBJECT_NAME_INVALID: return SP_ERR_SYS_INVALID; + case SP_NT_STATUS_OBJECT_NAME_INVALID: + case SP_NT_STATUS_NOT_A_REPARSE_POINT: return SP_ERR_SYS_INVALID; case SP_NT_STATUS_NOT_A_DIRECTORY: return SP_ERR_SYS_NOT_DIR; case SP_NT_STATUS_FILE_IS_A_DIRECTORY: return SP_ERR_SYS_IS_DIR; case SP_NT_STATUS_OBJECT_NAME_COLLISION: return SP_ERR_SYS_EXISTS; @@ -7132,6 +7221,11 @@ SP_PRIVATE sp_err_t sp_sys_nt_set_name_info(sp_sys_fd_t from_fd, sp_str_t from, sp_sys_nt_close(handle); + // NT reports a target whose parent exists but is not a directory as + // INVALID_PARAMETER, for both the Ex and legacy classes and for links. A + // missing parent is PATH_NOT_FOUND. This is NT's ENOTDIR for these two + // primitives; elsewhere INVALID_PARAMETER still means a bug on our side. + if (status == SP_NT_STATUS_INVALID_PARAMETER) return SP_ERR_SYS_NOT_DIR; return sp_sys_err_from_nt(status); } @@ -10458,6 +10552,90 @@ sp_err_t sp_sys_symlink_s(sp_str_t existing, sp_sys_fd_t to_fd, sp_str_t alias) return sp_sys_symlink(existing.data, existing.len, to_fd, alias.data, alias.len); } +///////////////////// +// SP_SYS_READLINK // +///////////////////// +sp_err_t sp_sys_readlink_p(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len) { + *target_len = 0; + +#if defined(SP_WIN32) + sp_sys_fd_t handle = SP_SYS_INVALID_FD; + sp_nt_status_t status = sp_sys_nt_open( + fd, + sp_str(path, len), + FILE_READ_ATTRIBUTES | SYNCHRONIZE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + SP_NT_FILE_OPEN, + SP_NT_FILE_SYNCHRONOUS_IO_NONALERT | SP_NT_FILE_OPEN_REPARSE_POINT | SP_NT_FILE_OPEN_FOR_BACKUP_INTENT, + 0, + &handle + ); + if (!SP_NT_SUCCESS(status)) return sp_sys_err_from_nt(status); + + SP_ALIGNED u8 rdb_buf [SP_NT_MAX_REPARSE_DATA]; + sp_nt_io_status_block_t iosb = sp_zero; + status = SP_NT(NtFsControlFile)( + (void*)handle, SP_NULLPTR, SP_NULLPTR, SP_NULLPTR, &iosb, + SP_NT_FSCTL_GET_REPARSE_POINT, SP_NULLPTR, 0, rdb_buf, sizeof(rdb_buf) + ); + sp_sys_nt_close(handle); + if (!SP_NT_SUCCESS(status)) return sp_sys_err_from_nt(status); + + sp_nt_symlink_reparse_buffer_t* rdb = (sp_nt_symlink_reparse_buffer_t*)rdb_buf; + if (rdb->ReparseTag != SP_NT_IO_REPARSE_TAG_SYMLINK) return SP_ERR_SYS_INVALID; + + u16* names = (u16*)(rdb_buf + sizeof(sp_nt_symlink_reparse_buffer_t)); + u16* name = names + rdb->SubstituteNameOffset / sizeof(u16); + u32 name_len = rdb->SubstituteNameLength / (u32)sizeof(u16); + sp_wide_str_t wide = { .data = name, .len = name_len }; + if (!(rdb->Flags & SP_NT_SYMLINK_FLAG_RELATIVE)) { + wide = sp_sys_dos_from_nt_wtf16(name, name_len); + } + + SP_ALIGNED c8 utf8_buf [SP_NT_MAX_REPARSE_DATA * 3 / 2]; + sp_mem_fixed_t fixed = sp_mem_fixed(utf8_buf, sizeof(utf8_buf)); + sp_str_t utf8 = sp_zero; + sp_try(sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), wide, &utf8)); + if (utf8.len >= size) return SP_ERR_SYS_NAME_TOO_LONG; + + sp_mem_copy(buf, utf8.data, utf8.len); + *target_len = utf8.len; + return SP_OK; + +#elif defined(SP_LINUX) + c8 cpath [SP_PATH_MAX]; + sp_try(sp_sys_posix_path(path, len, cpath)); + s64 rc = sp_syscall(SP_SYSCALL_NUM_READLINKAT, fd, cpath, buf, size); + if (sp_sys_is_err(rc)) return sp_sys_err_from_errno(-rc); + if ((u64)rc == size) return SP_ERR_SYS_NAME_TOO_LONG; + *target_len = (u64)rc; + return SP_OK; + +#elif defined(SP_MACOS) || defined(SP_COSMO) + c8 cpath [SP_PATH_MAX]; + sp_try(sp_sys_posix_path(path, len, cpath)); + s64 rc = readlinkat((int)fd, cpath, buf, (size_t)size); + if (rc < 0) return sp_sys_err_from_errno(errno); + if ((u64)rc == size) return SP_ERR_SYS_NAME_TOO_LONG; + *target_len = (u64)rc; + return SP_OK; + +#elif defined(SP_WASM) + (void)fd; (void)path; (void)len; (void)buf; (void)size; + return SP_ERR_SYS_UNSUPPORTED; + +#else + #error "sp_sys_readlink" +#endif +} + +sp_err_t sp_sys_readlink_s(sp_sys_fd_t fd, sp_str_t path, c8* buf, u64 size, sp_str_t* target) { + u64 len = 0; + sp_err_t err = sp_sys_readlink(fd, path.data, path.len, buf, size, &len); + *target = sp_str(buf, (u32)len); + return err; +} + ////////////////// // SP_SYS_CHMOD // ////////////////// @@ -10505,10 +10683,19 @@ sp_err_t sp_sys_chmod_p(sp_sys_fd_t fd, const c8* path, u32 len, const sp_sys_fi #endif } +sp_err_t sp_sys_set_times_p(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_timespec_t atime, sp_sys_timespec_t mtime) { + (void)fd; (void)path; (void)len; (void)atime; (void)mtime; + return SP_ERR_SYS_UNSUPPORTED; +} + sp_err_t sp_sys_chmod_s(sp_sys_fd_t fd, sp_str_t path, const sp_sys_file_meta_t* st) { return sp_sys_chmod(fd, path.data, path.len, st); } +sp_err_t sp_sys_set_times_s(sp_sys_fd_t fd, sp_str_t path, sp_sys_timespec_t atime, sp_sys_timespec_t mtime) { + return sp_sys_set_times(fd, path.data, path.len, atime, mtime); +} + /////////////////////////////// // SP_SYS_CANONICALIZE_PATH // /////////////////////////////// @@ -10535,17 +10722,11 @@ s64 sp_sys_canonicalize_path_p(const c8* path, u32 len, c8* buf, u64 size) { sp_sys_nt_close(handle); if (wlen == 0 || wlen >= SP_PATH_MAX) return -1; - // This code is wrong. Stripping \\?\ is wrong for input paths that - // are legitimately UNC. The right path is to do what dunce does in Rust, - // which is classify the path first, but I'm leaving this for now because - // it only affects callers who explicitly pass in a UNC path. If you're - // using regular C:/ paths, it's OK - u32 skip = 0; - if (wlen >= 4 && wbuf[0] == '\\' && wbuf[1] == '\\' && wbuf[2] == '?' && wbuf[3] == '\\') skip = 4; + sp_wide_str_t wide = sp_sys_dos_from_nt_wtf16(wbuf, wlen); SP_ALIGNED c8 u8buf[SP_PATH_MAX * 3 + 1]; sp_mem_fixed_t fixed = sp_mem_fixed(u8buf, sizeof(u8buf)); sp_str_t utf8; - if (sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), (sp_wide_str_t) { .data = wbuf + skip, .len = wlen - skip }, &utf8) != SP_OK) return -1; + if (sp_wtf16_to_wtf8(sp_mem_fixed_as_allocator(&fixed), wide, &utf8) != SP_OK) return -1; if (utf8.len >= size) return -1; sp_mem_copy(buf, utf8.data, utf8.len); buf[utf8.len] = 0; @@ -17063,7 +17244,13 @@ sp_err_t sp_io_file_reader_as_file(sp_io_reader_t* r, sp_sys_fd_t* fd, u64** pos sp_err_t sp_io_file_reader_discard(sp_io_reader_t* reader, u64 n, u64* discarded) { sp_io_file_reader_t* r = (sp_io_file_reader_t*)reader; - u64 remaining = r->pos < r->size ? r->size - r->pos : 0; + u64 size = 0; + sp_err_t err = sp_io_file_reader_size(r, &size); + if (err) { + if (discarded) *discarded = 0; + return err; + } + u64 remaining = r->pos < size ? size - r->pos : 0; u64 skip = sp_min(n, remaining); r->pos += skip; if (discarded) *discarded = skip; @@ -17081,12 +17268,6 @@ sp_err_t sp_io_file_reader_from_file(sp_io_file_reader_t* r, sp_sys_fd_t file, s .close_mode = mode, }; - sp_err_t err = sp_io_file_reader_size_force(r, SP_NULLPTR); - if (err) { - if (mode == SP_IO_CLOSE_MODE_AUTO) sp_sys_close(file); - *r = sp_zero_s(sp_io_file_reader_t); - return err; - } return SP_OK; } @@ -17112,7 +17293,16 @@ sp_err_t sp_io_file_reader_seek(sp_io_file_reader_t* r, s64 offset, sp_io_whence switch (whence) { case SP_IO_SEEK_SET: pos = offset; break; case SP_IO_SEEK_CUR: pos = (s64)r->pos + offset; break; - case SP_IO_SEEK_END: pos = (s64)r->size + offset; break; + case SP_IO_SEEK_END: { + u64 size = 0; + err = sp_io_file_reader_size(r, &size); + if (err) { + pos = -1; + goto done; + } + pos = (s64)size + offset; + break; + } } if (pos < 0) { @@ -17133,6 +17323,7 @@ sp_err_t sp_io_file_reader_seek_cb(sp_io_reader_t* reader, s64 offset, sp_io_whe } sp_err_t sp_io_file_reader_size(sp_io_file_reader_t* r, u64* size) { + if (!r->size_known) return sp_io_file_reader_size_force(r, size); if (size) *size = r->size; return SP_OK; } @@ -17145,6 +17336,7 @@ sp_err_t sp_io_file_reader_size_force(sp_io_file_reader_t* r, u64* size) { return err; } r->size = (u64)st.size; + r->size_known = true; if (size) *size = r->size; return SP_OK; } @@ -17659,7 +17851,7 @@ sp_err_t sp_io_file_writer_write(sp_io_writer_t* writer, const void* ptr, u64 si u64 num_bytes = 0; sp_err_t err = sp_sys_pwrite(w->fd, ptr, size, w->pos, &num_bytes); w->pos += num_bytes; - if (w->pos > w->size) w->size = w->pos; + if (w->size_known && w->pos > w->size) w->size = w->pos; if (bytes_written) *bytes_written = num_bytes; return err; } @@ -17669,7 +17861,7 @@ sp_err_t sp_io_file_writer_transfer(sp_io_writer_t* writer, sp_sys_fd_t fd, u64* u64 moved = 0; sp_err_t err = sp_sys_transfer_positional(fd, pos, w->fd, count, w->pos, &moved); w->pos += moved; - if (w->pos > w->size) w->size = w->pos; + if (w->size_known && w->pos > w->size) w->size = w->pos; if (bytes_moved) *bytes_moved = moved; return err; } @@ -17682,7 +17874,16 @@ sp_err_t sp_io_file_writer_seek(sp_io_file_writer_t* w, s64 offset, sp_io_whence switch (whence) { case SP_IO_SEEK_SET: new_pos = offset; break; case SP_IO_SEEK_CUR: new_pos = (s64)w->pos + offset; break; - case SP_IO_SEEK_END: new_pos = (s64)w->size + offset; break; + case SP_IO_SEEK_END: { + u64 size = 0; + sp_err_t err = sp_io_file_writer_size(w, &size); + if (err) { + if (position) *position = -1; + return err; + } + new_pos = (s64)size + offset; + break; + } } if (new_pos < 0) { @@ -17697,6 +17898,7 @@ sp_err_t sp_io_file_writer_seek(sp_io_file_writer_t* w, s64 offset, sp_io_whence sp_err_t sp_io_file_writer_size(sp_io_file_writer_t* w, u64* size) { sp_assert(w); + if (!w->size_known) return sp_io_file_writer_size_force(w, size); if (size) *size = w->size; return SP_OK; } @@ -17709,6 +17911,7 @@ sp_err_t sp_io_file_writer_size_force(sp_io_file_writer_t* w, u64* size) { return err; } w->size = (u64)st.size; + w->size_known = true; if (size) *size = w->size; return SP_OK; } @@ -17734,12 +17937,6 @@ sp_err_t sp_io_file_writer_from_fd(sp_io_file_writer_t* w, sp_sys_fd_t fd, sp_io .close_mode = close_mode, }; - sp_err_t err = sp_io_file_writer_size_force(w, SP_NULLPTR); - if (err) { - if (close_mode == SP_IO_CLOSE_MODE_AUTO) sp_sys_close(fd); - *w = sp_zero_s(sp_io_file_writer_t); - return err; - } return SP_OK; } @@ -17752,7 +17949,9 @@ sp_err_t sp_io_file_writer_from_path(sp_io_file_writer_t* w, sp_str_t path) { return err; } - return sp_io_file_writer_from_fd(w, fd, SP_IO_CLOSE_MODE_AUTO); + sp_try(sp_io_file_writer_from_fd(w, fd, SP_IO_CLOSE_MODE_AUTO)); + w->size_known = true; // truncated on open + return SP_OK; } sp_err_t sp_io_writer_set_buffer(sp_io_writer_t* writer, u8* ptr, u64 size) { @@ -17865,24 +18064,23 @@ sp_err_t sp_io_write_c8(sp_io_writer_t* writer, c8 c) { return sp_io_write_all(writer, &c, 1, SP_NULLPTR); } +#define SP_IO_PAD_CHUNK 4096 + sp_err_t sp_io_pad(sp_io_writer_t* writer, u64 size, u64* bytes_written) { - static const u8 zeros [64] = sp_zero; + static const u8 zeros [SP_IO_PAD_CHUNK] = sp_zero; sp_assert(writer); sp_err_t result = SP_OK; u64 total = 0; - sp_try_goto(sp_io_flush(writer), result, done); - while (total < size) { - u64 chunk = sp_min(size - total, 64); u64 chunk_written = 0; - sp_try_goto(sp_io_write(writer, zeros, chunk, &chunk_written), result, done); + result = sp_io_write_all(writer, zeros, sp_min(size - total, sizeof(zeros)), &chunk_written); total += chunk_written; + if (result) break; } -done: if (bytes_written) *bytes_written = total; return result; } @@ -19756,35 +19954,27 @@ sp_err_t sp_io_read_file(sp_mem_t mem, sp_str_t path, sp_str_t* content) { return err; } -sp_err_t sp_fs_create_dir(sp_str_t path) { - if (sp_str_empty(path)) return SP_ERR_SYS_NOT_FOUND; - if (sp_fs_exists(path)) { - return sp_fs_is_dir(path) ? SP_OK : SP_ERR_SYS_EXISTS; - } +SP_PRIVATE sp_err_t sp_fs_create_parent_at(sp_sys_fd_t dir, sp_str_t path); - sp_err_t result = SP_OK; - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_da(sp_str_t) missing = sp_da_new(s.mem, sp_str_t); +SP_PRIVATE sp_err_t sp_fs_mkdir_at(sp_sys_fd_t dir, sp_str_t path) { + sp_err_t err = sp_sys_mkdir_s(dir, path, 0755); + if (!err || err == SP_ERR_SYS_NOT_FOUND) return err; - // Walk up, collecting intermediate paths that don't exist - path = sp_fs_trim_path(path); - while (!sp_str_empty(path) && !sp_fs_is_root(path) && !sp_fs_exists(path)) { - sp_da_push(missing, path); - path = sp_fs_parent_path(path); - } + sp_sys_file_meta_t st = sp_zero; + if (sp_sys_get_path_metadata_s(dir, path, &st)) return err; + return st.kind == SP_FS_KIND_DIR ? SP_OK : err; +} - // Walk back down and create each one - sp_da_rfor(missing, it) { - result = sp_sys_mkdir_s(sp_sys_get_root(0), missing[it], 0755); - if (result) { - if (!sp_fs_is_dir(missing[it])) goto cleanup; - result = SP_OK; - } - } +SP_PRIVATE sp_err_t sp_fs_create_dir_at(sp_sys_fd_t dir, sp_str_t path) { + sp_err_t err = sp_fs_mkdir_at(dir, path); + if (err != SP_ERR_SYS_NOT_FOUND) return err; -cleanup: - sp_mem_end_scratch(s); - return result; + sp_try(sp_fs_create_parent_at(dir, path)); + return sp_fs_mkdir_at(dir, path); +} + +sp_err_t sp_fs_create_dir(sp_str_t path) { + return sp_fs_create_dir_at(sp_sys_get_root(0), sp_fs_trim_path(path)); } sp_err_t sp_fs_create_file(sp_str_t path) { @@ -19794,21 +19984,15 @@ sp_err_t sp_fs_create_file(sp_str_t path) { } sp_err_t sp_fs_create_file_slice(sp_str_t path, sp_mem_slice_t slice) { - sp_try(sp_fs_create_file(path)); sp_io_file_writer_t io = sp_zero; sp_try(sp_io_file_writer_from_path(&io, path)); - sp_try(sp_io_write_all(&io.base, slice.data, slice.len, SP_NULLPTR)); - sp_try(sp_io_file_writer_close(&io)); - return SP_OK; + sp_err_t written = sp_io_write_all(&io.base, slice.data, slice.len, SP_NULLPTR); + sp_err_t closed = sp_io_file_writer_close(&io); + return written ? written : closed; } sp_err_t sp_fs_create_file_str(sp_str_t path, sp_str_t str) { - sp_try(sp_fs_create_file(path)); - sp_io_file_writer_t io = sp_zero; - sp_try(sp_io_file_writer_from_path(&io, path)); - sp_try(sp_io_write_str(&io.base, str, SP_NULLPTR)); - sp_try(sp_io_file_writer_close(&io)); - return SP_OK; + return sp_fs_create_file_slice(path, sp_mem_slice((u8*)str.data, str.len)); } sp_err_t sp_fs_create_file_cstr(sp_str_t path, const c8* str) { @@ -19827,7 +20011,6 @@ sp_err_t sp_fs_link(sp_str_t from, sp_str_t to, sp_fs_link_kind_t kind) { switch (kind) { case SP_FS_LINK_HARD: return sp_fs_create_hard_link(from, to); case SP_FS_LINK_SYMBOLIC: return sp_fs_create_sym_link(from, to); - case SP_FS_LINK_COPY: return sp_fs_copy(from, to); } SP_UNREACHABLE_RETURN(SP_OK); } @@ -20022,60 +20205,249 @@ sp_err_t sp_fs_remove_dir(sp_str_t path) { return err; } -sp_err_t sp_fs_copy_file_meta(sp_str_t from, sp_str_t to, const sp_sys_file_meta_t* st) { - sp_err_t err = SP_OK; - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_io_file_reader_t reader = sp_zero; - sp_io_file_writer_t writer = sp_zero; +static sp_atomic_s32_t sp_fs_atomic_sequence; + +SP_PRIVATE sp_str_t sp_fs_temp_name(sp_str_t dir, sp_str_t name, c8 buf [SP_PATH_MAX]) { + sp_tm_epoch_t now = sp_tm_now_epoch(); + u64 stamp = (now.s << 20) ^ (u64)now.ns; + u64 sequence = (u64)(u32)sp_atomic_s32_add(&sp_fs_atomic_sequence, 1, SP_ATOMIC_SEQ_CST); - if (sp_fs_is_dir(to)) { - to = sp_fs_join_path(s.mem, to, sp_fs_get_name(from)); + sp_io_mem_writer_t io = sp_zero; + sp_io_mem_writer_from_buffer(&io, buf, SP_PATH_MAX); + if (sp_str_empty(dir)) { + sp_fmt_io(&io.base, ".{}.{}.{}.tmp", sp_fmt_str(name), sp_fmt_uint(stamp), sp_fmt_uint(sequence)); + } else { + sp_fmt_io(&io.base, "{}/.{}.{}.{}.tmp", sp_fmt_str(dir), sp_fmt_str(name), sp_fmt_uint(stamp), sp_fmt_uint(sequence)); } + return sp_io_mem_writer_as_str(&io); +} + +SP_PRIVATE sp_err_t sp_fs_create_parent_at(sp_sys_fd_t dir, sp_str_t path) { + sp_str_t parent = sp_fs_parent_path(path); + if (sp_str_empty(parent)) return SP_OK; + sp_err_t err = sp_fs_create_dir_at(dir, parent); + return err == SP_ERR_SYS_EXISTS ? SP_ERR_SYS_NOT_DIR : err; +} + +SP_PRIVATE sp_err_t sp_fs_atomic_create(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_str_t path, sp_str_t temp_dir) { + *af = sp_zero_s(sp_fs_atomic_t); + af->dir = dir; + af->path = path; + af->temp = sp_fs_temp_name(temp_dir, sp_fs_get_name(path), af->temp_buf); - sp_try_goto(sp_io_file_reader_from_path(&reader, from), err, done); - err = sp_io_file_writer_from_path(&writer, to); + u32 flags = SP_SYS_OPEN_CREATE | SP_SYS_OPEN_EXCLUSIVE; + sp_sys_fd_t fd = SP_SYS_INVALID_FD; + sp_err_t err = sp_sys_open_s(dir, af->temp, SP_SYS_OPEN_MODE_WO, flags, &fd); + if (err == SP_ERR_SYS_NOT_FOUND) { + err = sp_fs_create_parent_at(dir, af->temp); + if (!err) err = sp_sys_open_s(dir, af->temp, SP_SYS_OPEN_MODE_WO, flags, &fd); + } + if (err) { + *af = sp_zero_s(sp_fs_atomic_t); + return err; + } + + err = sp_io_file_writer_from_fd(&af->writer, fd, SP_IO_CLOSE_MODE_AUTO); + if (err) { + sp_sys_unlink_s(dir, af->temp); + *af = sp_zero_s(sp_fs_atomic_t); + } + return err; +} + +sp_err_t sp_fs_atomic_open_at(sp_fs_atomic_t* af, sp_sys_fd_t dir, sp_str_t path) { + return sp_fs_atomic_create(af, dir, path, sp_fs_parent_path(path)); +} + +sp_err_t sp_fs_atomic_open(sp_fs_atomic_t* af, sp_str_t path) { + return sp_fs_atomic_create(af, sp_sys_get_root(0), path, sp_fs_parent_path(path)); +} + +sp_err_t sp_fs_atomic_open_staged(sp_fs_atomic_t* af, sp_str_t path, sp_str_t staging) { + if (sp_str_empty(staging)) return SP_ERR_SYS_BUG; + return sp_fs_atomic_create(af, sp_sys_get_root(0), path, staging); +} + +sp_io_writer_t* sp_fs_atomic_writer(sp_fs_atomic_t* af) { + return &af->writer.base; +} + +SP_PRIVATE sp_err_t sp_fs_atomic_publish(sp_fs_atomic_t* af, sp_fs_atomic_mode_t mode) { + switch (mode) { + case SP_FS_ATOMIC_REPLACE: + return sp_sys_rename_s(af->dir, af->temp, af->dir, af->path); + case SP_FS_ATOMIC_EXCLUSIVE: { + sp_err_t err = sp_sys_link_s(af->dir, af->temp, af->dir, af->path); + if (!err) sp_sys_unlink_s(af->dir, af->temp); + return err; + } + } + SP_UNREACHABLE_RETURN(SP_OK); +} + +sp_err_t sp_fs_atomic_commit(sp_fs_atomic_t* af, sp_fs_atomic_mode_t mode) { + // Flush explicitly so a failed flush (e.g. ENOSPC) is reported -- close() + // swallows the flush error and only surfaces a failed close of the fd. + sp_err_t err = sp_io_flush(&af->writer.base); + sp_err_t close_err = sp_io_file_writer_close(&af->writer); + if (!err) err = close_err; + + if (!err) { + err = sp_fs_atomic_publish(af, mode); + if (err == SP_ERR_SYS_NOT_FOUND) { + err = sp_fs_create_parent_at(af->dir, af->path); + if (!err) err = sp_fs_atomic_publish(af, mode); + } + } + + if (err) sp_sys_unlink_s(af->dir, af->temp); + af->temp = sp_zero_s(sp_str_t); + return err; +} + +sp_err_t sp_fs_atomic_abort(sp_fs_atomic_t* af) { + if (sp_str_empty(af->temp)) return SP_OK; + sp_io_file_writer_close(&af->writer); + sp_err_t err = sp_sys_unlink_s(af->dir, af->temp); + af->temp = sp_zero_s(sp_str_t); + return err; +} + +SP_PRIVATE sp_err_t sp_fs_atomic_write(sp_fs_atomic_t* af, const void* data, u64 size) { + sp_err_t err = sp_io_write_all(sp_fs_atomic_writer(af), data, size, SP_NULLPTR); + if (err) { + sp_fs_atomic_abort(af); + return err; + } + return sp_fs_atomic_commit(af, SP_FS_ATOMIC_REPLACE); +} + +sp_err_t sp_fs_write_atomic_slice_staged(sp_str_t path, sp_str_t staging, sp_mem_slice_t slice) { + sp_fs_atomic_t af = sp_zero; + sp_try(sp_fs_atomic_open_staged(&af, path, staging)); + return sp_fs_atomic_write(&af, slice.data, slice.len); +} + +sp_err_t sp_fs_write_atomic_slice(sp_str_t path, sp_mem_slice_t slice) { + sp_fs_atomic_t af = sp_zero; + sp_try(sp_fs_atomic_open(&af, path)); + return sp_fs_atomic_write(&af, slice.data, slice.len); +} + +sp_err_t sp_fs_write_atomic(sp_str_t path, sp_str_t str) { + sp_fs_atomic_t af = sp_zero; + sp_try(sp_fs_atomic_open(&af, path)); + return sp_fs_atomic_write(&af, str.data, str.len); +} + +sp_err_t sp_fs_write_atomic_cstr(sp_str_t path, const c8* str) { + return sp_fs_write_atomic(path, sp_str_view(str)); +} + +SP_PRIVATE sp_err_t sp_fs_copy_bytes(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode, const sp_sys_file_meta_t* meta) { + sp_io_file_reader_t reader = sp_zero; + sp_try(sp_io_file_reader_from_path(&reader, from)); + + sp_fs_atomic_t af = sp_zero; + sp_err_t err = sp_fs_atomic_create(&af, sp_sys_get_root(0), to, sp_fs_parent_path(to)); if (err) { sp_io_file_reader_close(&reader); - goto done; + return err; } - u8 buffer[4096]; - while (true) { - u64 bytes_read = 0; - sp_err_t read_err = sp_io_read(&reader.base, buffer, sizeof(buffer), &bytes_read); - if (bytes_read) { - err = sp_io_write_all(&writer.base, buffer, bytes_read, SP_NULLPTR); - if (err) break; + err = sp_io_copy(sp_fs_atomic_writer(&af), &reader.base, SP_NULLPTR); + sp_io_file_reader_close(&reader); + if (!err) err = sp_sys_chmod_s(af.dir, af.temp, meta); + if (err) { + sp_fs_atomic_abort(&af); + return err; + } + return sp_fs_atomic_commit(&af, mode); +} + +SP_PRIVATE sp_err_t sp_fs_copy_tree_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { + sp_sys_file_meta_t meta = sp_zero; + sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); + return sp_fs_copy_bytes(from, to, mode, &meta); +} + +SP_PRIVATE sp_err_t sp_fs_copy_link(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { + sp_sys_fd_t root = sp_sys_get_root(0); + + c8 buf [SP_PATH_MAX]; + sp_str_t target = sp_zero; + sp_try(sp_sys_readlink_s(root, from, buf, sizeof(buf), &target)); + + switch (mode) { + case SP_FS_ATOMIC_REPLACE: { + c8 temp_buf [SP_PATH_MAX]; + sp_str_t temp = sp_fs_temp_name(sp_fs_parent_path(to), sp_fs_get_name(to), temp_buf); + sp_try(sp_sys_symlink_s(target, root, temp)); + sp_err_t err = sp_sys_rename_s(root, temp, root, to); + if (err) sp_sys_unlink_s(root, temp); + return err; } - if (read_err) { - if (read_err != SP_ERR_IO_EOF) err = read_err; - break; + case SP_FS_ATOMIC_EXCLUSIVE: { + return sp_sys_symlink_s(target, root, to); } } + SP_UNREACHABLE_RETURN(SP_OK); +} - { - sp_err_t close_err = sp_io_file_reader_close(&reader); - if (!err) err = close_err; - close_err = sp_io_file_writer_close(&writer); - if (!err) err = close_err; +sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { + sp_sys_file_meta_t meta = sp_zero; + sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); + + switch (meta.kind) { + case SP_FS_KIND_FILE: return sp_fs_copy_bytes(from, to, mode, &meta); + case SP_FS_KIND_DIR: return SP_ERR_SYS_IS_DIR; + case SP_FS_KIND_SYMLINK: + case SP_FS_KIND_NONE: return SP_ERR_SYS_UNSUPPORTED; } - if (!err) err = sp_sys_chmod_s(sp_sys_get_root(0), to, st); + SP_UNREACHABLE_RETURN(SP_OK); +} -done: - sp_mem_end_scratch(s); - return err; +SP_PRIVATE bool sp_fs_path_within(sp_str_t path, sp_str_t root) { + return sp_str_starts_with(path, root) && (path.len == root.len || sp_fs_is_sep(path.data[root.len])); } -sp_err_t sp_fs_copy_file(sp_str_t from, sp_str_t to) { - sp_sys_file_meta_t st = sp_zero; - sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &st)); +SP_PRIVATE bool sp_fs_tree_contains(sp_mem_t mem, sp_str_t from, sp_str_t to) { + sp_str_t root = sp_fs_canonicalize_path(mem, from); + sp_str_t nearest = sp_zero; + while (sp_str_empty(nearest) && !sp_str_empty(to) && !sp_fs_is_root(to)) { + nearest = sp_fs_canonicalize_path(mem, to); + to = sp_fs_parent_path(to); + } + return sp_fs_path_within(nearest, root); +} - switch (st.kind) { - case SP_FS_KIND_FILE: return sp_fs_copy_file_meta(from, to, &st); - case SP_FS_KIND_DIR: return SP_ERR_SYS_IS_DIR; - // opening a fifo for read blocks, and a device has no defined size - default: return SP_ERR_SYS_UNSUPPORTED; +sp_err_t sp_fs_copy_tree(sp_str_t from, sp_str_t to, sp_fs_atomic_mode_t mode) { + from = sp_fs_trim_path(from); + to = sp_fs_trim_path(to); + u32 skip = from.len + !sp_fs_is_sep(sp_str_back(from)); + + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_fs_it_t it = sp_fs_it_new_recursive(s.mem, from); + + sp_err_t err = it.err; + if (!err && sp_fs_tree_contains(s.mem, from, to)) err = SP_ERR_SYS_INVALID; + if (!err) err = sp_fs_create_dir(to); + + for (; !err && sp_fs_it_valid(&it); sp_fs_it_next(&it)) { + sp_str_t rel = sp_str_sub(it.entry.path, (s32)skip, (s32)(it.entry.path.len - skip)); + sp_str_t dst = sp_fs_join_path(s.mem, to, rel); + switch (it.entry.kind) { + case SP_FS_KIND_DIR: err = sp_fs_create_dir(dst); break; + case SP_FS_KIND_FILE: err = sp_fs_copy_tree_file(it.entry.path, dst, mode); break; + case SP_FS_KIND_SYMLINK: err = sp_fs_copy_link(it.entry.path, dst, mode); break; + case SP_FS_KIND_NONE: err = SP_ERR_SYS_UNSUPPORTED; break; + } } + if (!err) err = it.err; + + sp_fs_it_deinit(&it); + sp_mem_end_scratch(s); + return err; } bool sp_fs_glob_match(sp_str_t name, sp_str_t glob) { @@ -20108,67 +20480,24 @@ bool sp_fs_glob_match(sp_str_t name, sp_str_t glob) { return g == glob.len; } -sp_err_t sp_fs_copy_glob(sp_str_t from, sp_str_t glob, sp_str_t to) { - sp_err_t err = SP_OK; - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - - sp_da(sp_fs_entry_t) entries; - err = sp_fs_collect(s.mem, from, &entries); - if (err) goto done; - - err = sp_fs_create_dir(to); - if (err) goto done; - - sp_da_for(entries, i) { - sp_fs_entry_t* entry = &entries[i]; - if (!sp_fs_glob_match(entry->name, glob)) continue; - - err = sp_fs_copy(entry->path, to); - if (err) break; - } - -done: - sp_mem_end_scratch(s); - return err; -} - -sp_err_t sp_fs_copy_dir(sp_str_t from, sp_str_t to) { - if (sp_fs_is_dir(to)) { - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - to = sp_fs_join_path(s.mem, to, sp_fs_get_name(from)); - sp_err_t err = sp_fs_copy_glob(from, sp_str_lit("*"), to); - sp_mem_end_scratch(s); - return err; - } - return sp_fs_copy_glob(from, sp_str_lit("*"), to); -} - sp_err_t sp_fs_copy(sp_str_t from, sp_str_t to) { - if (sp_fs_is_glob(from)) { - return sp_fs_copy_glob(sp_fs_parent_path(from), sp_fs_get_name(from), to); - } - sp_sys_file_meta_t meta = sp_zero; sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), from, &meta)); switch (meta.kind) { - case SP_FS_KIND_FILE: { - return sp_fs_copy_file_meta(from, to, &meta); - } - case SP_FS_KIND_DIR: { - sp_sys_file_meta_t to_st = sp_zero; - sp_try(sp_sys_get_path_metadata_s(sp_sys_get_root(0), to, &to_st)); - if (to_st.kind != SP_FS_KIND_DIR) return SP_ERR_SYS_NOT_DIR; - - sp_mem_arena_marker_t s = sp_mem_begin_scratch(); - sp_err_t err = sp_fs_copy_glob(from, sp_str_lit("*"), sp_fs_join_path(s.mem, to, sp_fs_get_name(from))); - sp_mem_end_scratch(s); - return err; - } - default: { - return SP_ERR_SYS_UNSUPPORTED; - } + case SP_FS_KIND_FILE: return sp_fs_copy_bytes(from, to, SP_FS_ATOMIC_REPLACE, &meta); + case SP_FS_KIND_DIR: return sp_fs_copy_tree(from, to, SP_FS_ATOMIC_REPLACE); + case SP_FS_KIND_SYMLINK: + case SP_FS_KIND_NONE: return SP_ERR_SYS_UNSUPPORTED; } + SP_UNREACHABLE_RETURN(SP_OK); +} + +sp_err_t sp_fs_copy_into(sp_str_t from, sp_str_t dir) { + sp_mem_arena_marker_t s = sp_mem_begin_scratch(); + sp_err_t err = sp_fs_copy(from, sp_fs_join_path(s.mem, dir, sp_fs_get_name(sp_fs_trim_path(from)))); + sp_mem_end_scratch(s); + return err; } sp_fs_it_t sp_fs_it_new(sp_mem_t mem, sp_str_t path) { diff --git a/spn.toml b/spn.toml index 7cfb93e1..732d2ee1 100644 --- a/spn.toml +++ b/spn.toml @@ -17,7 +17,7 @@ source = [ "test/test/resolve.c", "test/test/sweep.c", "test/test/tracking.c", "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/iter.c", "test/sys/futex.c", "test/sys/fd.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/self_path.c", - "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", + "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", "test/sys/readlink.c", "test/sys/socket/data.c", "test/sys/socket/listen.c", "test/sys/socket/desc.c", "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/vtable.c", "test/sys/wait/event.c", "test/sys/wait/wait.c", @@ -40,7 +40,7 @@ source = [ "test/main.c", "test/sys/sys.c", "test/sys/open.c", "test/sys/path.c", "test/sys/iter.c", "test/sys/futex.c", "test/sys/fd.c", "test/sys/nanosleep.c", "test/sys/timestamps.c", "test/sys/self_path.c", - "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", + "test/sys/pipe/data.c", "test/sys/pipe/blocking.c", "test/sys/transfer.c", "test/sys/readlink.c", "test/sys/socket/data.c", "test/sys/socket/listen.c", "test/sys/socket/desc.c", "test/sys/tty/contract.c", "test/sys/tty/mode.c", "test/sys/vtable.c", "test/sys/wait/event.c", "test/sys/wait/wait.c", diff --git a/test/fs/atomic.c b/test/fs/atomic.c new file mode 100644 index 00000000..91ff9878 --- /dev/null +++ b/test/fs/atomic.c @@ -0,0 +1,260 @@ +#include "fs.h" + +typedef enum { + OP_REPLACE, + OP_EXCLUSIVE, + OP_ABORT, +} op_t; + +typedef struct { + sp_err_t err; + fs_expected_path_t paths [FS_MAX_PATHS]; +} expect_t; + +typedef struct { + const c8* name; + fs_setup_t setup [FS_MAX_SETUP]; + const c8* path; + const c8* content; + op_t op; + expect_t expect; +} test_t; + +// Every path is relative to a directory fd opened on the sandbox, never to +// the process root. +static const test_t open_at_tests [] = { + { + .name = "nested_parents_created", + .path = "A/B/C", + .content = "C", + .op = OP_REPLACE, + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + }, + }, + }, + { + .name = "replace_existing", + .setup = { + { .path = "A", .content = "old" }, + }, + .path = "A", + .content = "new", + .op = OP_REPLACE, + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "new" }, + }, + }, + }, + { + .name = "exclusive_new", + .path = "A/B", + .content = "B", + .op = OP_EXCLUSIVE, + .expect = { + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + }, + { + .name = "exclusive_existing", + .setup = { + { .path = "A", .content = "old" }, + }, + .path = "A", + .content = "new", + .op = OP_EXCLUSIVE, + .expect = { + .err = SP_ERR_SYS_EXISTS, + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "old" }, + }, + }, + }, + { + .name = "parent_is_file", + .setup = { + { .path = "P", .content = "P" }, + }, + .path = "P/B", + .content = "B", + .op = OP_REPLACE, + .expect = { + .err = SP_ERR_SYS_NOT_DIR, + .paths = { + { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, + }, + }, + }, + { + .name = "abort_leaves_nothing", + .path = "A/B", + .content = "B", + .op = OP_ABORT, + .expect = { + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B" }, + }, + }, + }, +}; + +// The temp lives in a staging directory "S" under the sandbox, so the +// destination's parent is first probed by the commit, not the open. +static const test_t staged_tests [] = { + { + .name = "parent_created_at_commit", + .path = "A/B/C", + .content = "C", + .op = OP_REPLACE, + .expect = { + .paths = { + { .path = "S", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "A/B/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + }, + }, + }, + { + .name = "exclusive_parent_created_at_commit", + .path = "A/B", + .content = "B", + .op = OP_EXCLUSIVE, + .expect = { + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + }, + { + .name = "exclusive_existing", + .setup = { + { .path = "A", .content = "old" }, + }, + .path = "A", + .content = "new", + .op = OP_EXCLUSIVE, + .expect = { + .err = SP_ERR_SYS_EXISTS, + .paths = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "old" }, + }, + }, + }, + { + .name = "parent_is_file", + .setup = { + { .path = "P", .content = "P" }, + }, + .path = "P/B", + .content = "B", + .op = OP_REPLACE, + .expect = { + .err = SP_ERR_SYS_NOT_DIR, + .paths = { + { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, + }, + }, + }, + { + .name = "exclusive_parent_is_file", + .setup = { + { .path = "P", .content = "P" }, + }, + .path = "P/B", + .content = "B", + .op = OP_EXCLUSIVE, + .expect = { + .err = SP_ERR_SYS_NOT_DIR, + .paths = { + { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, + }, + }, + }, + { + .name = "staging_is_file", + .setup = { + { .path = "S", .content = "S" }, + }, + .path = "A", + .content = "A", + .op = OP_REPLACE, + .expect = { + .err = SP_ERR_SYS_NOT_DIR, + .paths = { + { .path = "S", .exists = true, .kind = SP_FS_KIND_FILE, .content = "S" }, + }, + }, + }, + { + .name = "abort_leaves_nothing", + .path = "A/B", + .content = "B", + .op = OP_ABORT, + .expect = { + .paths = { + { .path = "A" }, + { .path = "A/B" }, + }, + }, + }, +}; + +static sp_err_t drive(sp_fs_atomic_t* af, sp_err_t err, const test_t* it) { + if (!err) err = sp_io_write_str(sp_fs_atomic_writer(af), sp_str_view(it->content), SP_NULLPTR); + if (!err) { + switch (it->op) { + case OP_REPLACE: err = sp_fs_atomic_commit(af, SP_FS_ATOMIC_REPLACE); break; + case OP_EXCLUSIVE: err = sp_fs_atomic_commit(af, SP_FS_ATOMIC_EXCLUSIVE); break; + case OP_ABORT: err = sp_fs_atomic_abort(af); break; + } + } + return err; +} + +sp_test_each(fs, atomic_open_at, test_t, open_at_tests) { + sp_str_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_sys_fd_t dir = SP_SYS_INVALID_FD; + sp_must_ok(t, sp_sys_open_dir_s(sp_sys_get_root(0), sandbox, &dir)); + + sp_fs_atomic_t af = sp_zero; + sp_err_t err = drive(&af, sp_fs_atomic_open_at(&af, dir, sp_str_view(it->path)), it); + sp_sys_close(dir); + + sp_expect_err_eq(t, err, it->expect.err); + fs_expect_paths(t, sandbox, it->expect.paths); + fs_expect_no_temps(t, sandbox); + return SP_OK; +} + +sp_test_each(fs, atomic_staged, test_t, staged_tests) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t sandbox = sp_test_dir(t); + fs_apply_setup(t, sandbox, it->setup); + + sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_view(it->path)); + sp_str_t staging = sp_fs_join_path(mem, sandbox, sp_str_lit("S")); + + sp_fs_atomic_t af = sp_zero; + sp_err_t err = drive(&af, sp_fs_atomic_open_staged(&af, path, staging), it); + + sp_expect_err_eq(t, err, it->expect.err); + fs_expect_paths(t, sandbox, it->expect.paths); + fs_expect_no_temps(t, sandbox); + return SP_OK; +} + +sp_test(fs, atomic_staged_empty_staging_is_bug) { + sp_str_t path = sp_fs_join_path(sp_test_arena(t), sp_test_dir(t), sp_str_lit("A")); + + sp_fs_atomic_t af = sp_zero; + sp_expect_err_eq(t, sp_fs_atomic_open_staged(&af, path, sp_str_lit("")), SP_ERR_SYS_BUG); + return SP_OK; +} diff --git a/test/fs/copy.c b/test/fs/copy.c index 3bb59b70..a658f91b 100644 --- a/test/fs/copy.c +++ b/test/fs/copy.c @@ -2,14 +2,16 @@ typedef enum { OP_COPY_FILE, + OP_COPY_TREE, OP_COPY, - OP_LINK, + OP_COPY_INTO, } op_t; typedef struct { const c8* name; fs_setup_t setup [FS_MAX_SETUP]; op_t op; + sp_fs_atomic_mode_t mode; const c8* src; const c8* dst; sp_err_t err; @@ -30,20 +32,120 @@ static const test_t tests [] = { }, }, { - .name = "file_via_link", + .name = "file_empty", + .setup = { + { .path = "A" }, + }, + .src = "A", + .dst = "B", + .expect = { + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "" }, + }, + }, + { + .name = "file_replaces_existing", .setup = { { .path = "A", .content = "A" }, + { .path = "B", .content = "B" }, }, - .op = OP_LINK, .src = "A", .dst = "B", + .expect = { + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + { + .name = "file_creates_parents", + .setup = { + { .path = "A", .content = "A" }, + }, + .src = "A", + .dst = "D/E/B", + .expect = { + { .path = "D/E", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "D/E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + { + .name = "file_onto_itself", + .setup = { + { .path = "A", .content = "A" }, + }, + .src = "A", + .dst = "A", .expect = { { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + { + .name = "file_exclusive_new", + .setup = { + { .path = "A", .content = "A" }, + }, + .mode = SP_FS_ATOMIC_EXCLUSIVE, + .src = "A", + .dst = "B", + .expect = { { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, }, }, { - .name = "file_through_symlink", + .name = "file_exclusive_existing", + .setup = { + { .path = "A", .content = "A" }, + { .path = "B", .content = "B" }, + }, + .mode = SP_FS_ATOMIC_EXCLUSIVE, + .src = "A", + .dst = "B", + .err = SP_ERR_SYS_EXISTS, + .expect = { + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "file_exclusive_dest_is_dir", + .setup = { + { .path = "A", .content = "A" }, + { "B", FS_SETUP_DIR }, + }, + .mode = SP_FS_ATOMIC_EXCLUSIVE, + .src = "A", + .dst = "B", + .err = SP_ERR_SYS_EXISTS, + .expect = { + { .path = "B", .exists = true, .kind = SP_FS_KIND_DIR }, + }, + }, + { + .name = "file_dest_parent_is_symlink", + .setup = { + { .path = "A", .content = "A" }, + { "D", FS_SETUP_DIR }, + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "D" }, + }, + .src = "A", + .dst = "L/B", + .expect = { + { .path = "D/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + { + .name = "file_dest_is_symlink", + .setup = { + { .path = "A", .content = "A" }, + { .path = "T", .content = "T" }, + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "T" }, + }, + .src = "A", + .dst = "L", + .expect = { + { .path = "L", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + { .path = "T", .exists = true, .kind = SP_FS_KIND_FILE, .content = "T" }, + }, + }, + { + .name = "file_source_symlink_followed", .setup = { { .path = "A", .content = "A" }, { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, @@ -54,13 +156,28 @@ static const test_t tests [] = { { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, }, }, + { + .name = "file_source_dangling_symlink", + .setup = { + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + }, + .src = "L", + .dst = "B", + .err = SP_ERR_SYS_NOT_FOUND, + .expect = { + { .path = "B" }, + }, + }, { .name = "file_source_missing", + .setup = { + { .path = "B", .content = "B" }, + }, .src = "A", .dst = "B", .err = SP_ERR_SYS_NOT_FOUND, .expect = { - { .path = "B" }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, }, }, { @@ -89,79 +206,327 @@ static const test_t tests [] = { { .path = "B" }, }, }, +#endif { - .name = "source_is_fifo", + .name = "file_dest_parent_is_file", .setup = { - { .path = "F", .kind = FS_SETUP_FIFO }, + { .path = "A", .content = "A" }, + { .path = "P", .content = "P" }, }, - .op = OP_COPY, - .src = "F", + .src = "A", + .dst = "P/B", + .err = SP_ERR_SYS_NOT_DIR, + .expect = { + { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, + }, + }, + { + .name = "file_dest_is_dir", + .setup = { + { .path = "A", .content = "A" }, + { "B", FS_SETUP_DIR }, + }, + .src = "A", .dst = "B", - .err = SP_ERR_SYS_UNSUPPORTED, + .err = SP_ERR_SYS_IS_DIR, .expect = { - { .path = "B" }, + { .path = "B", .exists = true, .kind = SP_FS_KIND_DIR }, }, }, -#endif { - .name = "dir_basic", + .name = "tree_nested", .setup = { { "A", FS_SETUP_DIR }, - { .path = "A/B", .content = "B" }, + { "A/B", FS_SETUP_DIR }, { .path = "A/C", .content = "C" }, - { "D", FS_SETUP_DIR }, + { .path = "A/B/D", .content = "D" }, }, - .op = OP_COPY, + .op = OP_COPY_TREE, .src = "A", - .dst = "D", + .dst = "E", .expect = { - { .path = "D/A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "D/A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, - { .path = "D/A/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + { .path = "E", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "E/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + { .path = "E/B", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "E/B/D", .exists = true, .kind = SP_FS_KIND_FILE, .content = "D" }, + }, + }, + { + .name = "tree_empty", + .setup = { + { "A", FS_SETUP_DIR }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .expect = { + { .path = "E", .exists = true, .kind = SP_FS_KIND_DIR }, }, }, { - .name = "dir_nested", + .name = "tree_empty_subdir", .setup = { { "A", FS_SETUP_DIR }, { "A/B", FS_SETUP_DIR }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .expect = { + { .path = "E/B", .exists = true, .kind = SP_FS_KIND_DIR }, + }, + }, + { + .name = "tree_replaces_existing", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { "E", FS_SETUP_DIR }, + { .path = "E/B", .content = "E" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .expect = { + { .path = "E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "tree_exclusive_existing", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { "E", FS_SETUP_DIR }, + { .path = "E/B", .content = "E" }, + }, + .op = OP_COPY_TREE, + .mode = SP_FS_ATOMIC_EXCLUSIVE, + .src = "A", + .dst = "E", + .err = SP_ERR_SYS_EXISTS, + .expect = { + { .path = "E/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "E" }, + }, + }, + { + .name = "tree_symlink_preserved", + .setup = { + { "A", FS_SETUP_DIR }, { .path = "A/C", .content = "C" }, - { .path = "A/B/D", .content = "D" }, + { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "A/C" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .expect = { + { .path = "E/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "A/C" }, + }, + }, + { + .name = "tree_symlink_replaced", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/C", .content = "C" }, + { .path = "A/L", .kind = FS_SETUP_SYMLINK, .target = "A/C" }, { "E", FS_SETUP_DIR }, + { .path = "E/L", .kind = FS_SETUP_SYMLINK, .target = "A" }, }, - .op = OP_COPY, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .expect = { + { .path = "E/L", .exists = true, .kind = SP_FS_KIND_SYMLINK, .target = "A/C" }, + }, + }, + { + .name = "tree_into_itself", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/C", .content = "C" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "A/B", + .err = SP_ERR_SYS_INVALID, + .expect = { + { .path = "A/B" }, + }, + }, + { + .name = "tree_into_itself_through_symlink", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/C", .content = "C" }, + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "L/B", + .err = SP_ERR_SYS_INVALID, + .expect = { + { .path = "A/B" }, + }, + }, + { + .name = "tree_onto_itself", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/C", .content = "C" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "A", + .err = SP_ERR_SYS_INVALID, + .expect = { + { .path = "A/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + }, + }, + { + .name = "tree_beside_itself", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/C", .content = "C" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "A/../E", + .expect = { + { .path = "E/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, + }, + }, + { + .name = "tree_dest_parent_is_file", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { .path = "P", .content = "P" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "P/E", + .err = SP_ERR_SYS_NOT_DIR, + .expect = { + { .path = "P", .exists = true, .kind = SP_FS_KIND_FILE, .content = "P" }, + }, + }, + { + .name = "tree_dest_is_file", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { .path = "E", .content = "E" }, + }, + .op = OP_COPY_TREE, .src = "A", .dst = "E", + .err = SP_ERR_SYS_EXISTS, .expect = { - { .path = "E/A", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "E/A/C", .exists = true, .kind = SP_FS_KIND_FILE, .content = "C" }, - { .path = "E/A/B", .exists = true, .kind = SP_FS_KIND_DIR }, - { .path = "E/A/B/D", .exists = true, .kind = SP_FS_KIND_FILE, .content = "D" }, + { .path = "E", .exists = true, .kind = SP_FS_KIND_FILE, .content = "E" }, }, }, { - .name = "dir_with_nonalphanumeric", + .name = "tree_source_missing", + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .err = SP_ERR_SYS_NOT_FOUND, + .expect = { + { .path = "E" }, + }, + }, + { + .name = "tree_source_is_file", + .setup = { + { .path = "A", .content = "A" }, + }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .err = SP_ERR_SYS_NOT_DIR, + .expect = { + { .path = "E" }, + }, + }, +#if defined(SP_POSIX) + { + .name = "tree_fifo_inside", .setup = { - { "A.B", FS_SETUP_DIR }, - { "C", FS_SETUP_DIR }, + { "A", FS_SETUP_DIR }, + { .path = "A/F", .kind = FS_SETUP_FIFO }, }, + .op = OP_COPY_TREE, + .src = "A", + .dst = "E", + .err = SP_ERR_SYS_UNSUPPORTED, + .expect = { + { .path = "E/F" }, + }, + }, +#endif + { + .name = "dispatch_file", + .setup = { + { .path = "A", .content = "A" }, + }, + .op = OP_COPY, + .src = "A", + .dst = "B", + .expect = { + { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + }, + }, + { + .name = "dispatch_dir", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + }, + .op = OP_COPY, + .src = "A", + .dst = "D", + .expect = { + { .path = "D", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "D/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "dispatch_missing", .op = OP_COPY, - .src = "A.B", - .dst = "C", + .src = "A", + .dst = "B", + .err = SP_ERR_SYS_NOT_FOUND, .expect = { - { .path = "C/A.B", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "B" }, }, }, { - .name = "unicode_file", + .name = "into_dir", .setup = { - { .path = "\xc3\xb6riginal.txt", .content = "A" }, + { "A", FS_SETUP_DIR }, + { .path = "A/B", .content = "B" }, + { "D", FS_SETUP_DIR }, }, - .src = "\xc3\xb6riginal.txt", - .dst = "\xc3\xbc\x63opy.txt", + .op = OP_COPY_INTO, + .src = "A", + .dst = "D", .expect = { - { .path = "\xc3\xb6riginal.txt", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, - { .path = "\xc3\xbc\x63opy.txt", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, + { .path = "D/A", .exists = true, .kind = SP_FS_KIND_DIR }, + { .path = "D/A/B", .exists = true, .kind = SP_FS_KIND_FILE, .content = "B" }, + }, + }, + { + .name = "into_file", + .setup = { + { .path = "A", .content = "A" }, + { "D", FS_SETUP_DIR }, + }, + .op = OP_COPY_INTO, + .src = "A", + .dst = "D", + .expect = { + { .path = "D/A", .exists = true, .kind = SP_FS_KIND_FILE, .content = "A" }, }, }, }; @@ -173,44 +538,136 @@ sp_test_each(fs, copy, test_t, tests) { sp_str_t sandbox = sp_test_dir(t); fs_apply_setup(t, sandbox, it->setup); - sp_str_t src = sp_fs_join_path(mem, sandbox, sp_str_view(it->src)); - sp_str_t dst = sp_fs_join_path(mem, sandbox, sp_str_view(it->dst)); + sp_str_t src = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->src)); + sp_str_t dst = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(it->dst)); sp_err_t result = SP_OK; switch (it->op) { - case OP_COPY_FILE: result = sp_fs_copy_file(src, dst); break; + case OP_COPY_FILE: result = sp_fs_copy_file(src, dst, it->mode); break; + case OP_COPY_TREE: result = sp_fs_copy_tree(src, dst, it->mode); break; case OP_COPY: result = sp_fs_copy(src, dst); break; - case OP_LINK: result = sp_fs_link(src, dst, SP_FS_LINK_COPY); break; + case OP_COPY_INTO: result = sp_fs_copy_into(src, dst); break; } sp_expect_err_eq(t, result, it->err); fs_expect_paths(t, sandbox, it->expect); + fs_expect_no_temps(t, sandbox); + return SP_OK; +} + +typedef struct { + const c8* name; + u32 size; +} size_test_t; + +static const size_test_t sizes [] = { + { .name = "chunk_minus_one", .size = 4095 }, + { .name = "chunk", .size = 4096 }, + { .name = "chunk_plus_one", .size = 4097 }, + { .name = "large", .size = 1 << 20 }, +}; + +sp_test_each(fs, copy_size, size_test_t, sizes) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t from = fs_path_c(t, "A"); + sp_str_t to = fs_path_c(t, "B"); + + u8* data = sp_alloc_n(mem, u8, it->size); + sp_for(i, it->size) { + data[i] = (u8)(i * 31 + 7); + } + sp_must_ok(t, sp_fs_create_file_slice(from, sp_mem_slice(data, it->size))); + sp_must_ok(t, sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE)); + + sp_str_t copied = sp_zero; + sp_must_ok(t, sp_io_read_file(mem, to, &copied)); + sp_must_eq(t, copied.len, it->size); + sp_expect_mem_eq(t, copied.data, data, it->size); return SP_OK; } #if defined(SP_POSIX) -sp_test(fs, copy_preserves_file_attributes) { +// postcondition: dest has source's bytes and mode, on a fresh inode; timestamps unspecified +sp_test(fs, copy_file_preserves_mode, .serial = true) { sp_mem_t mem = sp_test_arena(t); - sp_str_t source = fs_path(t, sp_str_lit("A")); - sp_fs_create_file_str(source, sp_str_lit("A")); + sp_str_t from = fs_path_c(t, "A"); + sp_str_t to = fs_path_c(t, "B"); + sp_fs_create_file_str(from, sp_str_lit("A")); + + sp_must_eq(t, chmod(sp_cstr_from_str(mem, from), 0755), 0); + + struct stat from_stat = sp_zero; + sp_must_eq(t, stat(sp_cstr_from_str(mem, from), &from_stat), 0); + + mode_t mask = umask(077); + sp_err_t copied_err = sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE); + umask(mask); + sp_must_ok(t, copied_err); + + struct stat to_stat = sp_zero; + sp_must_eq(t, stat(sp_cstr_from_str(mem, to), &to_stat), 0); + sp_must_eq(t, to_stat.st_mode, from_stat.st_mode); + sp_must_eq(t, to_stat.st_size, from_stat.st_size); + sp_must(t, to_stat.st_ino != from_stat.st_ino); - sp_must_eq(t, chmod(sp_cstr_from_str(mem, source), 0755), 0); + sp_str_t copied = sp_zero; + sp_io_read_file(mem, to, &copied); + sp_expect_str_eq(t, copied, sp_str_lit("A")); + return SP_OK; +} - struct stat original_stat = sp_zero; - sp_must_eq(t, stat(sp_cstr_from_str(mem, source), &original_stat), 0); +sp_test(fs, copy_file_replaces_read_only_dest) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t from = fs_path_c(t, "A"); + sp_str_t to = fs_path_c(t, "B"); + sp_fs_create_file_str(from, sp_str_lit("A")); + sp_fs_create_file_str(to, sp_str_lit("B")); + sp_must_eq(t, chmod(sp_cstr_from_str(mem, to), 0444), 0); - sp_str_t copy = fs_path(t, sp_str_lit("B")); - sp_must_ok(t, sp_fs_copy(source, copy)); - sp_must(t, sp_fs_is_file(copy)); + sp_must_ok(t, sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE)); + + sp_str_t copied = sp_zero; + sp_io_read_file(mem, to, &copied); + sp_expect_str_eq(t, copied, sp_str_lit("A")); + return SP_OK; +} + +sp_test(fs, copy_file_unwritable_parent_leaves_nothing) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t from = fs_path_c(t, "A"); + sp_str_t dir = fs_path_c(t, "D"); + sp_str_t to = fs_path_c(t, "D/B"); + sp_fs_create_file_str(from, sp_str_lit("A")); + sp_must_ok(t, sp_fs_create_dir(dir)); + + const c8* dir_c = sp_cstr_from_str(mem, dir); + sp_must_eq(t, chmod(dir_c, 0555), 0); + sp_err_t result = sp_fs_copy_file(from, to, SP_FS_ATOMIC_REPLACE); + sp_must_eq(t, chmod(dir_c, 0755), 0); + + if (!result) return sp_test_skip(t, "directory permissions not enforced"); + sp_expect_err_eq(t, result, SP_ERR_SYS_ACCESS_DENIED); + sp_expect(t, !sp_fs_exists(to)); + fs_expect_no_temps(t, sp_test_dir(t)); + return SP_OK; +} + +sp_test(fs, copy_tree_unreadable_subdir_fails) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t from = fs_path_c(t, "A"); + sp_str_t locked = fs_path_c(t, "A/B"); + sp_str_t to = fs_path_c(t, "E"); + sp_must_ok(t, sp_fs_create_dir(locked)); + sp_must_ok(t, sp_fs_create_file_str(fs_path_c(t, "A/B/C"), sp_str_lit("C"))); - struct stat copy_stat = sp_zero; - sp_must_eq(t, stat(sp_cstr_from_str(mem, copy), ©_stat), 0); + const c8* locked_c = sp_cstr_from_str(mem, locked); + sp_must_eq(t, chmod(locked_c, 0), 0); + sp_err_t result = sp_fs_copy_tree(from, to, SP_FS_ATOMIC_REPLACE); + sp_must_eq(t, chmod(locked_c, 0755), 0); - sp_must_eq(t, original_stat.st_mode, copy_stat.st_mode); - sp_must_eq(t, original_stat.st_size, copy_stat.st_size); - sp_str_t preserved = sp_zero; - sp_io_read_file(mem, copy, &preserved); - sp_expect_str_eq(t, preserved, sp_str_lit("A")); + if (!result) return sp_test_skip(t, "directory permissions not enforced"); + sp_expect_err_eq(t, result, SP_ERR_SYS_ACCESS_DENIED); + sp_expect(t, !sp_fs_exists(fs_path_c(t, "E/B/C"))); return SP_OK; } #endif diff --git a/test/fs/copy_glob.c b/test/fs/copy_glob.c deleted file mode 100644 index 384f2332..00000000 --- a/test/fs/copy_glob.c +++ /dev/null @@ -1,106 +0,0 @@ -#include "fs.h" - -typedef struct { - const c8* name; - bool missing_src; - fs_setup_t src [FS_MAX_SETUP]; - const c8* glob; - sp_err_t err; - fs_expected_path_t expect [FS_MAX_PATHS]; -} test_t; - -static const test_t tests [] = { - { - .name = "star", - .src = { - { "A" }, - { "B" }, - }, - .glob = "*", - .expect = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B", .exists = true, .kind = SP_FS_KIND_FILE }, - }, - }, - { - .name = "exact_name", - .src = { - { "A" }, - { "B" }, - }, - .glob = "A", - .expect = { - { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B" }, - }, - }, - { - .name = "star_suffix", - .src = { - { "A.C" }, - { "B.C" }, - { "D.E" }, - }, - .glob = "*.C", - .expect = { - { .path = "A.C", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "B.C", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "D.E" }, - }, - }, - { - .name = "star_prefix", - .src = { - { "AB" }, - { "AC" }, - { "D" }, - }, - .glob = "A*", - .expect = { - { .path = "AB", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "AC", .exists = true, .kind = SP_FS_KIND_FILE }, - { .path = "D" }, - }, - }, - { - .name = "no_match", - .src = { - { "A" }, - { "B" }, - }, - .glob = "C", - .expect = { - { .path = "A" }, - { .path = "B" }, - }, - }, - { - .name = "empty_src", - .glob = "*", - }, - { - .name = "missing_src", - .missing_src = true, - .glob = "*", - .err = SP_ERR_SYS_NOT_FOUND, - }, -}; - -sp_test_each(fs, copy_glob, test_t, tests) { - sp_mem_t mem = sp_test_arena(t); - sp_str_t sandbox = sp_test_dir(t); - sp_str_t src = sp_fs_join_path(mem, sandbox, sp_str_lit("src")); - sp_str_t dst = sp_fs_join_path(mem, sandbox, sp_str_lit("dst")); - if (!it->missing_src) { - sp_fs_create_dir(src); - fs_apply_setup(t, src, it->src); - } - - sp_expect_err_eq(t, sp_fs_copy_glob(src, sp_str_view(it->glob), dst), it->err); - - // the destination is created by copy_glob itself, and only after the - // source has been read successfully - sp_expect_eq(t, sp_fs_is_dir(dst), it->err == SP_OK); - fs_expect_paths(t, dst, it->expect); - return SP_OK; -} diff --git a/test/fs/create_dir.c b/test/fs/create_dir.c index 47588f4a..0a90e7e8 100644 --- a/test/fs/create_dir.c +++ b/test/fs/create_dir.c @@ -64,13 +64,7 @@ static const test_t tests [] = { }, .target = "A/B", .expect = { - // mkdir under a file component: POSIX reports ENOTDIR; NT path resolution - // reports STATUS_OBJECT_PATH_NOT_FOUND -#if defined(SP_WIN32) - .err = SP_ERR_SYS_NOT_FOUND, -#else .err = SP_ERR_SYS_NOT_DIR, -#endif .paths = { { .path = "A", .exists = true, .kind = SP_FS_KIND_FILE }, { .path = "A/B" }, @@ -86,13 +80,38 @@ static const test_t tests [] = { }, .target = "L", .expect = { - .err = SP_ERR_SYS_EXISTS, .paths = { { .path = "A", .exists = true, .kind = SP_FS_KIND_DIR }, { .path = "L", .exists = true, .kind = SP_FS_KIND_SYMLINK }, }, }, }, + { + .name = "destination_under_symlink_to_directory", + .setup = { + { "A", FS_SETUP_DIR }, + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + }, + .target = "L/B", + .expect = { + .paths = { + { .path = "A/B", .exists = true, .kind = SP_FS_KIND_DIR }, + }, + }, + }, + { + .name = "destination_is_dangling_symlink", + .setup = { + { .path = "L", .kind = FS_SETUP_SYMLINK, .target = "A" }, + }, + .target = "L", + .expect = { + .err = SP_ERR_SYS_EXISTS, + .paths = { + { .path = "A" }, + }, + }, + }, { .name = "destination_is_symlink_to_file", .setup = { diff --git a/test/fs/fs.h b/test/fs/fs.h index 8e4fd945..61058ab0 100644 --- a/test/fs/fs.h +++ b/test/fs/fs.h @@ -61,7 +61,6 @@ typedef enum { FS_SETUP_FILE, FS_SETUP_DIR, FS_SETUP_SYMLINK, - FS_SETUP_HARD_LINK, FS_SETUP_FIFO, } fs_setup_kind_t; @@ -79,6 +78,7 @@ typedef struct { bool exists; sp_fs_kind_t kind; const c8* content; + const c8* target; } fs_expected_path_t; static bool fs_setup_needs_symlinks(const fs_setup_t setup [FS_MAX_SETUP]) { @@ -146,13 +146,6 @@ static void fs_apply_setup(sp_test_t* t, sp_str_t sandbox, const fs_setup_t setu } break; } - case FS_SETUP_HARD_LINK: { - sp_str_t target = sp_fs_join_path(mem, sandbox, sp_str_view(ent->target)); - if (sp_fs_create_hard_link(target, path) != SP_OK) { - sp_test_fail(t, "failed to create hard link {} -> {}", sp_fmt_str(path), sp_fmt_str(target)); - } - break; - } case FS_SETUP_FIFO: { #if defined(SP_POSIX) if (mkfifo(sp_cstr_from_str(mem, path), 0644) != 0) { @@ -167,6 +160,16 @@ static void fs_apply_setup(sp_test_t* t, sp_str_t sandbox, const fs_setup_t setu } } +static void fs_expect_no_temps(sp_test_t* t, sp_str_t sandbox) { + sp_da(sp_fs_entry_t) entries = sp_zero; + sp_expect_ok(t, sp_fs_collect_recursive(sp_test_arena(t), sandbox, &entries)); + sp_da_for(entries, it) { + if (sp_str_ends_with(entries[it].name, sp_str_lit(".tmp"))) { + sp_test_fail(t, "temp file left behind: {}", sp_fmt_str(entries[it].path)); + } + } +} + static void fs_expect_paths(sp_test_t* t, sp_str_t sandbox, const fs_expected_path_t expected [FS_MAX_PATHS]) { sp_mem_t mem = sp_test_arena(t); sp_for(it, FS_MAX_PATHS) { @@ -196,6 +199,17 @@ static void fs_expect_paths(sp_test_t* t, sp_str_t sandbox, const fs_expected_pa }); } } + + if (info->target) { + sp_str_t expected = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(info->target)); +#if defined(SP_WIN32) + expected = sp_str_replace_c8(mem, expected, '/', '\\'); +#endif + c8 buf [SP_PATH_MAX]; + sp_str_t actual = sp_zero; + sp_expect_ok(t, sp_sys_readlink_s(sp_sys_get_root(0), path, buf, sizeof(buf), &actual)); + sp_expect_str_eq(t, actual, expected); + } } } diff --git a/test/io/file.c b/test/io/file.c index c0a1f3de..046df888 100644 --- a/test/io/file.c +++ b/test/io/file.c @@ -477,6 +477,98 @@ UTEST_F(io, read_file_larger_than_single_read) { // // The only way this surfaces is if you mix positional and streaming IO on the same kernel // handle, which is definitely a user error. +// Constructing over an fd is free: no syscall is made until something asks +// for the size, and the answer is then memoized. +UTEST_F(io, file_reader_from_file_defers_stat) { + sp_io_file_writer_t w = sp_zero; + sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_write(&w.base, "abc", 3, SP_NULLPTR); + sp_io_file_writer_close(&w); + + sp_sys_fd_t fd = SP_SYS_INVALID_FD; + ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_RO, 0, &fd), SP_OK); + + sp_io_file_reader_t r = sp_zero; + EXPECT_EQ(sp_io_file_reader_from_file(&r, fd, SP_IO_CLOSE_MODE_AUTO), SP_OK); + EXPECT_FALSE(r.size_known); + + u64 size = 0; + EXPECT_EQ(sp_io_file_reader_size(&r, &size), SP_OK); + EXPECT_EQ(size, (u64)3); + EXPECT_TRUE(r.size_known); + sp_io_file_reader_close(&r); +} + +UTEST_F(io, file_writer_from_fd_defers_stat) { + sp_io_file_writer_t w = sp_zero; + sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_write(&w.base, "abc", 3, SP_NULLPTR); + sp_io_file_writer_close(&w); + + sp_sys_fd_t fd = SP_SYS_INVALID_FD; + ASSERT_EQ(sp_sys_open_s(sp_sys_get_root(0), ut.file_path, SP_SYS_OPEN_MODE_WO, 0, &fd), SP_OK); + + sp_io_file_writer_t a = sp_zero; + EXPECT_EQ(sp_io_file_writer_from_fd(&a, fd, SP_IO_CLOSE_MODE_AUTO), SP_OK); + EXPECT_FALSE(a.size_known); + + s64 pos = 0; + EXPECT_EQ(sp_io_file_writer_seek(&a, 0, SP_IO_SEEK_END, &pos), SP_OK); + EXPECT_EQ(pos, (s64)3); + EXPECT_TRUE(a.size_known); + sp_io_file_writer_close(&a); +} + +// The size is fetched once, on first need, and memoized: after the first +// query it is answered from the struct even if the file has since grown. +// size_force is the explicit refresh. +UTEST_F(io, file_reader_size_is_memoized) { + sp_io_file_writer_t w = sp_zero; + sp_io_file_writer_from_path(&w, ut.file_path); + sp_io_write(&w.base, "abc", 3, SP_NULLPTR); + sp_io_flush(&w.base); + + sp_io_file_reader_t r = sp_zero; + sp_io_file_reader_from_path(&r, ut.file_path); + EXPECT_FALSE(r.size_known); + + u64 size = 0; + EXPECT_EQ(sp_io_file_reader_size(&r, &size), SP_OK); + EXPECT_EQ(size, (u64)3); + EXPECT_TRUE(r.size_known); + + sp_io_write(&w.base, "def", 3, SP_NULLPTR); + sp_io_flush(&w.base); + + EXPECT_EQ(sp_io_file_reader_size(&r, &size), SP_OK); + EXPECT_EQ(size, (u64)3); + EXPECT_EQ(sp_io_file_reader_size_force(&r, &size), SP_OK); + EXPECT_EQ(size, (u64)6); + + sp_io_file_reader_close(&r); + sp_io_file_writer_close(&w); +} + +// A writer opened by path truncates, so it knows the size is zero without +// asking; writes then keep the size current. +UTEST_F(io, file_writer_from_path_knows_size) { + sp_io_file_writer_t w = sp_zero; + sp_io_file_writer_from_path(&w, ut.file_path); + EXPECT_TRUE(w.size_known); + EXPECT_EQ(w.size, (u64)0); + + sp_io_write(&w.base, "abcd", 4, SP_NULLPTR); + sp_io_flush(&w.base); + u64 size = 0; + EXPECT_EQ(sp_io_file_writer_size(&w, &size), SP_OK); + EXPECT_EQ(size, (u64)4); + + s64 pos = 0; + EXPECT_EQ(sp_io_file_writer_seek(&w, -1, SP_IO_SEEK_END, &pos), SP_OK); + EXPECT_EQ(pos, (s64)3); + sp_io_file_writer_close(&w); +} + UTEST_F(io, file_reader_positional_does_not_touch_kernel_cursor) { SKIP_ON_WIN32() sp_io_file_writer_t w = sp_zero; diff --git a/test/io/write.c b/test/io/write.c index e885f271..4470d13f 100644 --- a/test/io/write.c +++ b/test/io/write.c @@ -41,12 +41,18 @@ void run_io_mock_write_test(int* utest_result, io_mock_write_test_t t) { EXPECT_EQ(err, step->flush.err); break; } + case IO_STEP_PAD: { + u64 bytes = 0; + sp_err_t err = sp_io_pad(&w.base, step->pad.size, &bytes); + EXPECT_EQ(err, step->pad.err); + EXPECT_EQ(bytes, step->pad.bytes); + break; + } case IO_STEP_NONE: case IO_STEP_READ: case IO_STEP_SEEK: case IO_STEP_SIZE: - case IO_STEP_COPY: - case IO_STEP_PAD: { + case IO_STEP_COPY: { sp_unreachable_case(); } } @@ -190,6 +196,17 @@ UTEST_F(io_write, buffered_small_no_backend_call) { }); } +UTEST_F(io_write, buffered_pad_no_backend_call) { + run_io_mock_write_test(utest_result, (io_mock_write_test_t){ + .results = {0}, + .buffer = 8, + .steps = { + { .kind = IO_STEP_WRITE, .write = { "abc", SP_OK, 3 } }, + { .kind = IO_STEP_PAD, .pad = { .size = 3, .bytes = 3 } }, + }, + }); +} + // Multiple small writes accumulate in the buffer; backend stays untouched. UTEST_F(io_write, buffered_writes_accumulate) { run_io_mock_write_test(utest_result, (io_mock_write_test_t){ diff --git a/test/sys/readlink.c b/test/sys/readlink.c new file mode 100644 index 00000000..5107c696 --- /dev/null +++ b/test/sys/readlink.c @@ -0,0 +1,79 @@ +#include "sp.h" +#include "sp/sp_test.h" + +typedef enum { + SETUP_NONE, + SETUP_FILE, + SETUP_LINK, + SETUP_LINK_ABSOLUTE, +} setup_t; + +typedef struct { + sp_err_t err; + const c8* target; +} expect_t; + +typedef struct { + const c8* name; + setup_t setup; + const c8* target; + u64 size; + expect_t expect; +} test_t; + +static const test_t tests [] = { + { .name = "relative", .setup = SETUP_LINK, .target = "A", .expect = { .target = "A" } }, + { .name = "nested", .setup = SETUP_LINK, .target = "B/C", .expect = { .target = "B/C" } }, + { .name = "unicode", .setup = SETUP_LINK, .target = "\xc3\xa9", .expect = { .target = "\xc3\xa9" } }, + { .name = "absolute", .setup = SETUP_LINK_ABSOLUTE, .target = "A" }, + { .name = "too_small", .setup = SETUP_LINK, .target = "A", .size = 1, .expect = { .err = SP_ERR_SYS_NAME_TOO_LONG } }, + { .name = "missing", .expect = { .err = SP_ERR_SYS_NOT_FOUND } }, + { .name = "not_a_link", .setup = SETUP_FILE, .expect = { .err = SP_ERR_SYS_INVALID } }, +}; + +static sp_err_t run(sp_test_t* t, test_t* c) { + sp_mem_t mem = sp_test_arena(t); + sp_str_t sandbox = sp_test_dir(t); + sp_str_t path = sp_fs_join_path(mem, sandbox, sp_str_lit("L")); + sp_str_t expected = c->expect.target ? sp_cstr_as_str(c->expect.target) : sp_zero_s(sp_str_t); + + switch (c->setup) { + case SETUP_NONE: { + break; + } + case SETUP_FILE: { + sp_must_ok(t, sp_fs_create_file(path)); + break; + } + case SETUP_LINK: { + if (sp_fs_create_sym_link(sp_cstr_as_str(c->target), path)) { + return sp_test_skip(t, "symlinks not available"); + } + break; + } + case SETUP_LINK_ABSOLUTE: { + expected = sp_fs_join_path(mem, sandbox, sp_cstr_as_str(c->target)); + if (sp_fs_create_sym_link(expected, path)) { + return sp_test_skip(t, "symlinks not available"); + } + break; + } + } + + // sys returns the target as the OS stores it; on Windows the writer stores + // backslashes, and readlink does not translate them back. +#if defined(SP_WIN32) + expected = sp_str_replace_c8(mem, expected, '/', '\\'); +#endif + + c8 buf [SP_PATH_MAX]; + sp_str_t target = sp_zero; + sp_err_t err = sp_sys_readlink_s(sp_sys_get_root(0), path, buf, c->size ? c->size : sizeof(buf), &target); + if (err == SP_ERR_SYS_UNSUPPORTED) return sp_test_skip(t, "readlink not supported"); + + sp_expect_err_eq(t, err, c->expect.err); + sp_expect_str_eq(t, target, expected); + return SP_OK; +} + +sp_test_each_fn(sys, readlink, test_t, tests, run); diff --git a/test/sys/vtable.c b/test/sys/vtable.c index 19c58699..d7906bb6 100644 --- a/test/sys/vtable.c +++ b/test/sys/vtable.c @@ -99,6 +99,10 @@ static sp_err_t mock_symlink(const c8* existing, u32 existing_len, sp_sys_fd_t t return (sp_err_t)69; } +static sp_err_t mock_readlink(sp_sys_fd_t fd, const c8* path, u32 len, c8* buf, u64 size, u64* target_len) { + return (sp_err_t)69; +} + static sp_err_t mock_get_path_metadata(sp_sys_fd_t fd, const c8* path, u32 len, sp_sys_file_meta_t* st) { return (sp_err_t)69; } @@ -318,6 +322,7 @@ static const sp_sys_vtable_t mock = { .rename = mock_rename, .link = mock_link, .symlink = mock_symlink, + .readlink = mock_readlink, .get_path_metadata = mock_get_path_metadata, .get_link_metadata = mock_get_link_metadata, .get_file_metadata = mock_get_file_metadata, @@ -462,6 +467,11 @@ static s64 call_symlink(void) { return (s64)sp_sys_symlink(SP_NULLPTR, 0, 0, SP_NULLPTR, 0); } +static s64 call_readlink(void) { + u64 len = 0; + return (s64)sp_sys_readlink(0, SP_NULLPTR, 0, SP_NULLPTR, 0, &len); +} + static s64 call_get_path_metadata(void) { return (s64)sp_sys_get_path_metadata(0, SP_NULLPTR, 0, SP_NULLPTR); } @@ -695,6 +705,7 @@ static const test_t tests [] = { { "rename", call_rename, 69 }, { "link", call_link, 69 }, { "symlink", call_symlink, 69 }, + { "readlink", call_readlink, 69 }, { "get_path_metadata", call_get_path_metadata, 69 }, { "get_link_metadata", call_get_link_metadata, 69 }, { "get_file_metadata", call_get_file_metadata, 69 },