// Copyright 2007-2022 David Robillard // SPDX-License-Identifier: ISC #include "zix/filesystem.h" #include "../errno_status.h" #include "../system.h" #include "../zix_config.h" #include "zix/allocator.h" #include "zix/attributes.h" #include "zix/status.h" #if USE_FLOCK && USE_FILENO # include #endif #if USE_CLONEFILE # include # include #endif #if USE_REALPATH # include #endif #include #include #include #include #include #include #include #include #include #include #ifndef MAX # define MAX(a, b) (((a) > (b)) ? (a) : (b)) #endif static inline ZixStatus zix_posix_status(const int rc) { return rc ? zix_errno_status(errno) : ZIX_STATUS_SUCCESS; } static uint32_t zix_get_block_size(const struct stat s1, const struct stat s2) { const blksize_t b1 = s1.st_blksize; const blksize_t b2 = s2.st_blksize; return (b1 > 0 && b2 > 0) ? (uint32_t)MAX(b1, b2) : 4096U; } static ZixStatus finish_copy(const int dst_fd, const int src_fd, const ZixStatus status) { const ZixStatus st0 = zix_posix_status(dst_fd >= 0 ? fdatasync(dst_fd) : 0); const ZixStatus st1 = zix_system_close_fds(dst_fd, src_fd); return status ? status : st0 ? st0 : st1; } static char* copy_path(ZixAllocator* const allocator, const char* const path, const size_t length) { char* result = NULL; if (path) { if ((result = (char*)zix_calloc(allocator, length + 1U, 1U))) { memcpy(result, path, length + 1U); } } return result; } #if !defined(PATH_MAX) && USE_PATHCONF static size_t max_path_size(void) { const long path_max = pathconf(path, _PC_PATH_MAX); return (path_max > 0) ? (size_t)path_max : zix_system_page_size(); } #elif !defined(PATH_MAX) static size_t max_path_size(void) { return zix_system_page_size(); } #endif #if USE_CLONEFILE static ZixStatus zix_clonefile(const char* const src, const char* const dst, const ZixCopyOptions options) { errno = 0; ZixStatus st = ZIX_STATUS_SUCCESS; const ZixFileType dst_type = zix_file_type(dst); const bool overwrite = (options == ZIX_COPY_OPTION_OVERWRITE_EXISTING); if (overwrite && dst_type == ZIX_FILE_TYPE_REGULAR) { st = zix_remove(dst); } else if (dst_type != ZIX_FILE_TYPE_NONE) { st = ZIX_STATUS_NOT_SUPPORTED; } return st ? st : zix_posix_status(clonefile(src, dst, 0)); } #endif #if USE_COPY_FILE_RANGE static ZixStatus zix_copy_file_range(const int src_fd, const int dst_fd, const size_t size) { errno = 0; size_t remaining = size; ssize_t r = 0; while (remaining > 0 && (r = copy_file_range(src_fd, NULL, dst_fd, NULL, remaining, 0U)) > 0) { remaining -= (size_t)r; } return (r >= 0) ? ZIX_STATUS_SUCCESS : zix_errno_status( (errno == EXDEV || errno == EINVAL) ? ENOSYS : errno); } #endif static ZixStatus copy_blocks(const int src_fd, const int dst_fd, void* const block, const size_t block_size) { ssize_t n_read = 0; while ((n_read = read(src_fd, block, block_size)) > 0) { if (write(dst_fd, block, (size_t)n_read) != n_read) { return zix_errno_status(errno); } } return ZIX_STATUS_SUCCESS; } ZixStatus zix_copy_file(ZixAllocator* const allocator, const char* const src, const char* const dst, const ZixCopyOptions options) { ZixStatus st = ZIX_STATUS_SUCCESS; (void)st; #if USE_CLONEFILE // Try to copy via the kernel on MacOS to take advantage of CoW st = zix_clonefile(src, dst, options); if (st != ZIX_STATUS_NOT_SUPPORTED) { return st; } #endif // Open source file and get its status const int src_fd = zix_system_open_fd(src, O_RDONLY, 0); struct stat src_stat; if (src_fd < 0 || fstat(src_fd, &src_stat)) { return finish_copy(-1, src_fd, zix_errno_status(errno)); } // Fail if the source is not a regular file (since we need a size) if (!S_ISREG(src_stat.st_mode)) { return finish_copy(-1, src_fd, ZIX_STATUS_BAD_ARG); } // Open a new destination file const bool overwrite = (options == ZIX_COPY_OPTION_OVERWRITE_EXISTING); const int dst_flags = O_WRONLY | O_CREAT | (overwrite ? O_TRUNC : O_EXCL); const int dst_fd = zix_system_open_fd(dst, dst_flags, 0644); struct stat dst_stat; if (dst_fd < 0 || fstat(dst_fd, &dst_stat)) { return finish_copy(dst_fd, src_fd, zix_errno_status(errno)); } #if USE_COPY_FILE_RANGE // Try to copy via the kernel on Linux/BSD to take advantage of CoW st = zix_copy_file_range(src_fd, dst_fd, (size_t)src_stat.st_size); if (st != ZIX_STATUS_NOT_SUPPORTED) { return finish_copy(dst_fd, src_fd, st); } #endif // Set sequential hints so the kernel can optimize the page cache #if USE_POSIX_FADVISE posix_fadvise(src_fd, 0, src_stat.st_size, POSIX_FADV_SEQUENTIAL); posix_fadvise(dst_fd, 0, src_stat.st_size, POSIX_FADV_SEQUENTIAL); #endif errno = 0; // Allocate a block for copying const size_t align = zix_system_page_size(); const uint32_t block_size = zix_get_block_size(src_stat, dst_stat); void* const block = zix_aligned_alloc(allocator, align, block_size); // Fall back to using a small stack buffer if allocation is unavailable char stack_buf[512]; void* const buffer = block ? block : stack_buf; const size_t buffer_size = block ? block_size : sizeof(stack_buf); // Copy file content one buffer at a time st = copy_blocks(src_fd, dst_fd, buffer, buffer_size); zix_aligned_free(NULL, block); return finish_copy(dst_fd, src_fd, st); } ZixStatus zix_create_directory(const char* const dir_path) { return (!dir_path[0]) ? ZIX_STATUS_BAD_ARG : zix_posix_status(mkdir(dir_path, 0777)); } ZixStatus zix_create_directory_like(const char* const dir_path, const char* const existing_path) { struct stat sb; return !dir_path[0] ? ZIX_STATUS_BAD_ARG : stat(existing_path, &sb) ? zix_errno_status(errno) : zix_posix_status(mkdir(dir_path, sb.st_mode)); } ZixStatus zix_create_hard_link(const char* const target_path, const char* const link_path) { return zix_posix_status(link(target_path, link_path)); } ZixStatus zix_create_symlink(const char* const target_path, const char* const link_path) { return zix_posix_status(symlink(target_path, link_path)); } ZixStatus zix_create_directory_symlink(const char* const target_path, const char* const link_path) { return zix_create_symlink(target_path, link_path); } char* zix_create_temporary_directory(ZixAllocator* const allocator, const char* const path_pattern) { const size_t length = strlen(path_pattern); char* const result = (char*)zix_calloc(allocator, length + 1U, 1U); if (result) { memcpy(result, path_pattern, length + 1U); if (!mkdtemp(result)) { zix_free(allocator, result); return NULL; } } return result; } ZixStatus zix_remove(const char* const path) { return zix_posix_status(remove(path)); } void zix_dir_for_each(const char* const path, void* const data, void (*const f)(const char* path, const char* name, void* data)) { DIR* dir = opendir(path); if (dir) { // NOLINTNEXTLINE(concurrency-mt-unsafe) for (struct dirent* entry = NULL; (entry = readdir(dir));) { if (!!strcmp(entry->d_name, ".") && !!strcmp(entry->d_name, "..")) { f(path, entry->d_name, data); } } closedir(dir); } } char* zix_canonical_path(ZixAllocator* const allocator, const char* const path) { if (!path) { return NULL; } #if USE_REALPATH && defined(PATH_MAX) // Some POSIX systems have a static PATH_MAX so we can resolve on the stack char buffer[PATH_MAX] = {0}; char* const canonical = realpath(path, buffer); if (canonical) { return copy_path(allocator, buffer, strlen(buffer)); } #elif USE_REALPATH && USE_PATHCONF // Others don't so we have to query PATH_MAX at runtime to allocate the result const size_t size = max_path_size(path); char* const buffer = (char*)zix_calloc(allocator, size, 1); char* const canonical = realpath(path, buffer); if (canonical) { return canonical; } zix_free(allocator, buffer); #endif return NULL; } ZixStatus zix_file_lock(FILE* const file, const ZixFileLockMode mode) { #if !defined(__EMSCRIPTEN__) && USE_FLOCK && USE_FILENO return zix_posix_status( flock(fileno(file), (mode == ZIX_FILE_LOCK_BLOCK) ? LOCK_EX : (LOCK_EX | LOCK_NB))); #else (void)file; (void)mode; return ZIX_STATUS_NOT_SUPPORTED; #endif } ZixStatus zix_file_unlock(FILE* const file, const ZixFileLockMode mode) { #if !defined(__EMSCRIPTEN__) && USE_FLOCK && USE_FILENO return zix_posix_status( flock(fileno(file), (mode == ZIX_FILE_LOCK_BLOCK) ? LOCK_UN : (LOCK_UN | LOCK_NB))); #else (void)file; (void)mode; return ZIX_STATUS_NOT_SUPPORTED; #endif } ZIX_CONST_FUNC static ZixFileType stat_file_type(const struct stat sb) { typedef struct { unsigned mask; ZixFileType type; } Mapping; static const Mapping map[] = { {S_IFREG, ZIX_FILE_TYPE_REGULAR}, {S_IFDIR, ZIX_FILE_TYPE_DIRECTORY}, {S_IFLNK, ZIX_FILE_TYPE_SYMLINK}, {S_IFBLK, ZIX_FILE_TYPE_BLOCK}, {S_IFCHR, ZIX_FILE_TYPE_CHARACTER}, {S_IFIFO, ZIX_FILE_TYPE_FIFO}, {S_IFSOCK, ZIX_FILE_TYPE_SOCKET}, {0U, ZIX_FILE_TYPE_UNKNOWN}, }; const unsigned mask = (unsigned)sb.st_mode & (unsigned)S_IFMT; unsigned m = 0U; while (map[m].mask && map[m].mask != mask) { ++m; } return map[m].type; } ZixFileType zix_file_type(const char* const path) { struct stat sb; return stat(path, &sb) ? ZIX_FILE_TYPE_NONE : stat_file_type(sb); } ZixFileType zix_symlink_type(const char* const path) { struct stat sb; return lstat(path, &sb) ? ZIX_FILE_TYPE_NONE : stat_file_type(sb); } char* zix_temp_directory_path(ZixAllocator* const allocator) { const char* tmpdir = getenv("TMPDIR"); // NOLINT(concurrency-mt-unsafe) tmpdir = tmpdir ? tmpdir : "/tmp"; return copy_path(allocator, tmpdir, strlen(tmpdir)); } char* zix_current_path(ZixAllocator* const allocator) { #if defined(PATH_MAX) // Some POSIX systems have a static PATH_MAX so we can store it on the stack char buffer[PATH_MAX] = {0}; return copy_path(allocator, getcwd(buffer, PATH_MAX), strlen(buffer)); #elif USE_PATHCONF // Others don't so we have to query PATH_MAX at runtime to allocate the result const size_t size = max_path_size(); char* const buffer = (char*)zix_calloc(allocator, size, 1); char* const current = getcwd(buffer, size); if (current) { return current; } zix_free(allocator, buffer); #endif return NULL; }