refactor: modularize and harden file I/O

This commit is contained in:
seregonwar committed 2026-09-09 18:13:06 +02:00
1 parent a62b2ad2b2
commit a042eae480
14 files changed
+1847 -2090

No files matched your search

+7 -1
View File
@@ -208,7 +208,11 @@ LIBS := $(PLATFORM_LIBS)
# Source files
SOURCES := src/platform/pal_network.c
SOURCES += src/platform/pal_fileio.c
SOURCES += src/platform/fileio/basic.c
SOURCES += src/platform/fileio/sendfile.c
SOURCES += src/platform/fileio/copy.c
SOURCES += src/platform/fileio/tree.c
SOURCES += src/platform/fileio/directory.c
SOURCES += src/platform/pal_alloc.c
SOURCES += src/platform/pal_scratch.c
SOURCES += src/platform/pal_notification.c
@@ -815,6 +819,8 @@ TEST_BINS += $(BUILD_DIR)/tests/test_instance
TEST_BINS += $(BUILD_DIR)/tests/test_chmod
TEST_BINS += $(BUILD_DIR)/tests/test_copy_atomic
TEST_BINS += $(BUILD_DIR)/tests/test_ftp_commands
TEST_BINS += $(BUILD_DIR)/tests/test_fileio_tree
TEST_BINS += $(BUILD_DIR)/tests/test_fileio_basic
ifeq ($(ENABLE_ZHTTPD),1)
TEST_BINS += $(BUILD_DIR)/tests/test_transfer
TEST_BINS += $(BUILD_DIR)/tests/test_http_api_common
+1
View File
@@ -78,6 +78,7 @@ typedef enum {
FTP_ERR_PROTOCOL = -22, /**< Protocol violation */
FTP_ERR_DIR_EXISTS = -23, /**< Directory already exists */
FTP_ERR_CROSS_DEVICE = -24, /**< Cross-device link (EXDEV) */
FTP_ERR_CANCELLED = -25, /**< Operation cancelled by caller */
FTP_ERR_UNKNOWN = -99, /**< Unknown error */
} ftp_error_t;
+8 -56
View File
@@ -24,15 +24,7 @@ SOFTWARE.
/**
* @file pal_fileio.h
* @brief Platform-agnostic file I/O with zero-copy support
*
* @author SeregonWar
* @version 1.0.0
* @date 2026-02-13
*
* OPTIMIZATION: Zero-copy file transfer via sendfile() where supported
* FALLBACK: Buffered read/write for platforms without sendfile()
*
* @brief Platform-agnostic file I/O and zero-copy transfer primitives.
*/
#ifndef PAL_FILEIO_H
@@ -61,6 +53,9 @@ SOFTWARE.
#if defined(__linux__)
#define HAS_SENDFILE 1
#include <sys/sendfile.h>
#elif defined(__APPLE__) && defined(__MACH__)
#define HAS_SENDFILE 1
#include <sys/socket.h>
#elif defined(__FreeBSD__) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
#define HAS_SENDFILE 1
#include <sys/uio.h>
@@ -68,45 +63,12 @@ SOFTWARE.
#define HAS_SENDFILE 0
#endif
/*===========================================================================*
* ZERO-COPY FILE TRANSFER
*===========================================================================*/
/**
* @brief Send file data via socket (zero-copy where supported)
*
* OPTIMIZATION: Uses sendfile() syscall when available for zero-copy
* kernel-to-socket transfer without userspace buffering.
*
* PERFORMANCE:
* - Zero-copy: ~950 MB/s on PS4, ~118 MB/s on PS5 (network-limited)
* - Buffered: ~300-400 MB/s (userspace copy overhead)
*
* @param sock_fd Socket file descriptor (destination)
* @param file_fd File descriptor (source)
* @param offset Starting offset in file (updated on return)
* @param count Number of bytes to send
*
* @return Number of bytes sent on success, negative error code on failure
* @retval >0 Number of bytes successfully sent
* @retval 0 End of file reached
* @retval -1 I/O error (check errno)
*
* @pre sock_fd >= 0 (valid socket)
* @pre file_fd >= 0 (valid file)
* @pre offset != NULL
* @pre *offset >= 0
* @pre count > 0
*
* @post *offset updated by number of bytes sent
*
* @note Thread-safety: Safe if file descriptors not shared
* @note WCET: Unbounded (depends on network/disk I/O)
*
* @warning Non-blocking sockets may return partial writes (EAGAIN)
* @warning Caller must handle partial transfers in loop
* Send up to count bytes starting at *offset and advance the offset by bytes sent.
* Callers must handle partial transfers and EAGAIN.
*/
ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count);
int pal_file_error_is_fatal(int error);
/*===========================================================================*
* FILE OPERATIONS
@@ -287,17 +249,7 @@ ftp_error_t pal_file_rename(const char *old_path, const char *new_path);
ftp_error_t pal_file_copy_recursive(const char *src, const char *dst,
int keep_src);
/*===========================================================================*
* COPY PROGRESS CALLBACK
*
* Invoked after each read/write chunk during file copy.
*
* bytes_copied : cumulative bytes written so far (across all files)
* user_data : opaque pointer passed by the caller
*
* Return: 0 = continue
* -1 = cancel (copy will abort and return FTP_ERR_CANCELLED)
*===========================================================================*/
/* Progress callbacks receive cumulative bytes; a negative return cancels the copy. */
typedef int (*pal_copy_progress_cb_t)(uint64_t bytes_copied, void *user_data);
+2 -15
View File
@@ -49,17 +49,6 @@ SOFTWARE.
extern int _fstatfs(int, struct statfs *);
#endif
/* Fallback: pal_fileio.h may be suppressed by a transitive include guard */
#ifndef PAL_FILE_WRITE_CHUNK_MAX
# if defined(PLATFORM_PS5) || defined(PS5)
# define PAL_FILE_WRITE_CHUNK_MAX 131072U /* 128 KB */
# elif defined(PLATFORM_PS4) || defined(PS4)
# define PAL_FILE_WRITE_CHUNK_MAX 65536U /* 64 KB */
# else
# define PAL_FILE_WRITE_CHUNK_MAX 262144U /* 256 KB */
# endif
#endif
/* PFS serializes inode creation on PS4/PS5. Serialize only O_CREAT opens
* in userspace to avoid multi-session journal contention and client timeouts. */
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
@@ -367,8 +356,7 @@ ftp_error_t cmd_RETR(ftp_session_t *session, const char *args) {
}
/* Retrying a bad vnode can panic PS4/PS5, so storage faults are terminal. */
if ((sent < 0) && ((errno == EIO) || (errno == ESTALE) ||
(errno == EBADF) || (errno == EFAULT))) {
if ((sent < 0) && pal_file_error_is_fatal(errno)) {
remaining = 1U;
break;
}
@@ -403,8 +391,7 @@ ftp_error_t cmd_RETR(ftp_session_t *session, const char *args) {
if ((r_sent < 0) && (errno == EINTR)) {
r--; /* don't count EINTR as a retry */
}
if ((r_sent < 0) && ((errno == EIO) || (errno == ESTALE) ||
(errno == EBADF) || (errno == EFAULT))) {
if ((r_sent < 0) && pal_file_error_is_fatal(errno)) {
break;
}
}
+264
View File
@@ -0,0 +1,264 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file basic.c @brief Basic file descriptor and metadata operations. */
#include "fileio_internal.h"
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#if defined(PLATFORM_PS5) || defined(PS5)
#define PAL_FILE_WRITE_CHUNK_MAX (1024U * 1024U)
#elif defined(PLATFORM_PS4) || defined(PS4)
#define PAL_FILE_WRITE_CHUNK_MAX (64U * 1024U)
#else
#define PAL_FILE_WRITE_CHUNK_MAX (256U * 1024U)
#endif
#if defined(PLATFORM_PS4) || defined(PS4)
#define PS4_SYS_FTRUNCATE 480
#endif
int pal_file_error_is_fatal(int error) {
return error == EIO || error == ESTALE || error == EBADF || error == EFAULT;
}
ftp_error_t fileio_error_from_errno(int error, ftp_error_t fallback) {
switch (error) {
case ENOENT:
return FTP_ERR_NOT_FOUND;
case EACCES:
case EPERM:
return FTP_ERR_PERMISSION;
case ENAMETOOLONG:
return FTP_ERR_PATH_TOO_LONG;
case ENOMEM:
case EMFILE:
case ENFILE:
return FTP_ERR_OUT_OF_MEMORY;
default:
return fallback;
}
}
int pal_file_open(const char *path, int flags, mode_t mode) {
if (path == NULL) {
errno = EINVAL;
return FTP_ERR_INVALID_PARAM;
}
size_t len = strlen(path);
if (len >= FTP_PATH_MAX) {
errno = ENAMETOOLONG;
return FTP_ERR_PATH_TOO_LONG;
}
int fd = open(path, flags, mode);
if (fd < 0) return fileio_error_from_errno(errno, FTP_ERR_FILE_OPEN);
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/* PS5 keeps read pages pager-backed for sendfile; PS4 uses F_NOCACHE only on reads. */
#if defined(PLATFORM_PS4)
#ifdef F_NOCACHE
if ((flags & O_WRONLY) == 0 && (flags & O_RDWR) == 0) {
(void)fcntl(fd, F_NOCACHE, 1);
}
#endif
#endif /* PLATFORM_PS4 only — explicitly excluded from PS5 */
#if defined(PLATFORM_PS5) && defined(POSIX_FADV_SEQUENTIAL)
if ((flags & O_WRONLY) == 0 && (flags & O_RDWR) == 0)
(void)posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
#endif
#endif
return fd;
}
/* FreeBSD/OrbisOS leaves an fd open after close(EINTR); Linux does not. */
ftp_error_t pal_file_close(int fd) {
if (fd < 0) {
return FTP_ERR_INVALID_PARAM;
}
#if defined(__FreeBSD__) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
{
unsigned retries = 0U;
const unsigned MAX_CLOSE_RETRIES = 8U;
while (close(fd) < 0) {
if (errno != EINTR) {
return FTP_ERR_FILE_WRITE;
}
if (++retries >= MAX_CLOSE_RETRIES) {
return FTP_ERR_FILE_WRITE;
}
}
}
#else
if (close(fd) < 0) {
if (errno != EINTR) {
return FTP_ERR_FILE_WRITE;
}
}
#endif
return FTP_OK;
}
ftp_error_t pal_file_stat(const char *path, struct stat *st) {
if ((path == NULL) || (st == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (stat(path, st) < 0) return fileio_error_from_errno(errno, FTP_ERR_FILE_STAT);
return FTP_OK;
}
ftp_error_t pal_file_fstat(int fd, struct stat *st) {
if ((fd < 0) || (st == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (fstat(fd, st) < 0) {
return FTP_ERR_FILE_STAT;
}
return FTP_OK;
}
ssize_t pal_file_read(int fd, void *buffer, size_t count) {
if (fd < 0 || (buffer == NULL && count != 0U)) {
errno = EINVAL;
return -1;
}
if (count == 0U) return 0;
return read(fd, buffer, count);
}
ssize_t pal_file_write(int fd, const void *buffer, size_t count) {
if (fd < 0 || (buffer == NULL && count != 0U)) {
errno = EINVAL;
return -1;
}
if (count == 0U) return 0;
return write(fd, buffer, count);
}
ssize_t pal_file_write_all(int fd, const void *buffer, size_t count) {
if (fd < 0 || (buffer == NULL && count != 0U)) {
errno = EINVAL;
return -1;
}
if (count == 0U) return 0;
const uint8_t *p = (const uint8_t *)buffer;
size_t total = 0U;
while (total < count) {
size_t remaining = count - total;
size_t chunk = remaining;
if (chunk > (size_t)PAL_FILE_WRITE_CHUNK_MAX) {
chunk = (size_t)PAL_FILE_WRITE_CHUNK_MAX;
}
ssize_t n = write(fd, p + total, chunk);
if (n > 0) {
total += (size_t)n;
continue;
}
if (n == 0) {
/* Some PFS builds report a full filesystem as write()==0. */
errno = ENOSPC;
return -1;
}
if (errno == EINTR) {
continue;
}
return -1;
}
return (ssize_t)total;
}
off_t pal_file_seek(int fd, off_t offset, int whence) {
if (fd < 0) {
errno = EINVAL;
return -1;
}
return lseek(fd, offset, whence);
}
ftp_error_t pal_file_truncate(int fd, off_t len) {
if ((fd < 0) || (len < 0)) {
return FTP_ERR_INVALID_PARAM;
}
#ifdef PLATFORM_PS4
if (syscall(PS4_SYS_FTRUNCATE, fd, len) < 0) {
return FTP_ERR_FILE_WRITE;
}
#else
if (ftruncate(fd, len) < 0) {
return FTP_ERR_FILE_WRITE;
}
#endif
return FTP_OK;
}
ftp_error_t pal_file_delete(const char *path) {
if (path == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (unlink(path) < 0) {
if (errno == EISDIR) return FTP_ERR_INVALID_PARAM;
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
}
return FTP_OK;
}
ftp_error_t pal_file_chmod(const char *path, mode_t mode) {
if (path == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (chmod(path, mode) < 0) {
int error = errno;
if (error == EINVAL
#if defined(EOPNOTSUPP)
|| error == EOPNOTSUPP
#elif defined(ENOTSUP)
|| error == ENOTSUP
#endif
) return FTP_ERR_PERMISSION;
return fileio_error_from_errno(error, FTP_ERR_FILE_WRITE);
}
return FTP_OK;
}
+565
View File
@@ -0,0 +1,565 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file copy.c @brief Atomic single-file copy pipeline. */
#include "pal_fileio.h"
#include "fileio_internal.h"
#include "ftp_log.h"
#include <errno.h>
#include <fcntl.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/statvfs.h>
#include <unistd.h>
/* File-to-file copies use large writes; network write chunk limits do not apply. */
#ifndef PAL_FILE_COPY_BUFFER_SIZE
#if defined(PLATFORM_PS5)
#define PAL_FILE_COPY_BUFFER_SIZE \
(4U * 1024U * \
1024U) /* 4 MB — NVMe ~215 MB/s, T_write=19ms covers T_read=12ms */
#elif defined(PLATFORM_PS4)
#define PAL_FILE_COPY_BUFFER_SIZE \
(1024U * 1024U) /* 1 MB — HDD ~85 MB/s, T_write=12ms covers T_read=3ms */
#else
#define PAL_FILE_COPY_BUFFER_SIZE (4U * 1024U * 1024U) /* 4 MB */
#endif
#endif
/* Large copies overlap source reads and destination writes. */
#include <pthread.h>
typedef struct {
uint8_t *buf[2]; /* two PAL_FILE_COPY_BUFFER_SIZE buffers */
size_t len[2]; /* bytes filled in each buffer (0 = free) */
int fill_idx; /* index reader is filling right now */
int src_fd; /* source file descriptor */
size_t buf_sz; /* PAL_FILE_COPY_BUFFER_SIZE */
int done; /* reader set to 1 on EOF or error */
int reader_err; /* errno from reader (0 = ok) */
pthread_mutex_t mtx;
pthread_cond_t cv_ready; /* writer waits: "buffer filled and ready" */
pthread_cond_t cv_free; /* reader waits: "buffer drained and free" */
} copy_pipe_t;
static void *copy_reader_thread(void *arg) {
copy_pipe_t *p = (copy_pipe_t *)arg;
pthread_mutex_lock(&p->mtx);
for (;;) {
int fi = p->fill_idx;
while ((p->len[fi] != 0U) && (p->done == 0)) {
pthread_cond_wait(&p->cv_free, &p->mtx);
}
if (p->done != 0) {
break;
}
pthread_mutex_unlock(&p->mtx);
ssize_t n;
do {
n = read(p->src_fd, p->buf[fi], p->buf_sz);
} while ((n < 0) && (errno == EINTR));
pthread_mutex_lock(&p->mtx);
if (n < 0) {
p->reader_err = errno;
p->done = 1;
pthread_cond_signal(&p->cv_ready);
break;
}
if (n == 0) {
p->done = 1;
pthread_cond_signal(&p->cv_ready);
break;
}
p->len[fi] = (size_t)n;
p->fill_idx = 1 - fi;
pthread_cond_signal(&p->cv_ready);
}
pthread_mutex_unlock(&p->mtx);
return NULL;
}
#define PAL_COPY_PIPELINE_MIN_BYTES ((uint64_t)PAL_FILE_COPY_BUFFER_SIZE * 2U)
static int copy_write_full(int fd, const uint8_t *data, size_t size,
int *saved_errno) {
size_t offset = 0U;
while (offset < size) {
ssize_t n = write(fd, data + offset, size - offset);
if (n > 0) {
offset += (size_t)n;
continue;
}
if (n < 0 && errno == EINTR) continue;
if (saved_errno != NULL) *saved_errno = (n == 0) ? ENOSPC : errno;
return -1;
}
return 0;
}
static ftp_error_t copy_report_progress(uint64_t bytes, uint64_t *cumulative,
pal_copy_progress_cb_t cb,
void *user_data) {
uint64_t total = bytes;
if (cumulative != NULL) {
*cumulative += bytes;
total = *cumulative;
}
if (cb != NULL && cb(total, user_data) < 0) return FTP_ERR_CANCELLED;
return FTP_OK;
}
static atomic_uint_fast32_t g_tmp_counter = ATOMIC_VAR_INIT(0U);
static ftp_error_t build_temp_path(const char *dst_path, char *out,
size_t out_size) {
char parent[FTP_PATH_MAX];
ftp_error_t err = fileio_parent_path(dst_path, parent, sizeof(parent));
if (err != FTP_OK) return err;
char name[64];
uint_fast32_t counter = atomic_fetch_add(&g_tmp_counter, 1U);
int n = snprintf(name, sizeof(name), ".zftpd.%lu.%lu.tmp",
(unsigned long)getpid(), (unsigned long)counter);
if (n < 0 || (size_t)n >= sizeof(name)) return FTP_ERR_PATH_TOO_LONG;
return fileio_join_path(parent, name, out, out_size);
}
static ftp_error_t create_temp_file(const char *dst_path, mode_t mode,
char *tmp_path, size_t tmp_size,
int *fd_out, int *out_errno) {
if (fd_out == NULL) return FTP_ERR_INVALID_PARAM;
for (unsigned attempt = 0U; attempt < 16U; attempt++) {
ftp_error_t err = build_temp_path(dst_path, tmp_path, tmp_size);
if (err != FTP_OK) return err;
int flags = O_WRONLY | O_CREAT | O_EXCL;
#ifndef PLATFORM_PS4
#ifndef PLATFORM_PS5
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
#endif
#endif
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
int fd = open(tmp_path, flags, mode);
if (fd >= 0) { *fd_out = fd; return FTP_OK; }
int error = errno;
if (error == EEXIST) continue;
if (out_errno != NULL) *out_errno = error;
return fileio_error_from_errno(error, FTP_ERR_FILE_OPEN);
}
if (out_errno != NULL) *out_errno = EEXIST;
return FTP_ERR_FILE_OPEN;
}
/*===========================================================================*
* FILE OPERATIONS
*===========================================================================*/
ftp_error_t
pal_file_copy_atomic_ex(const char *src_path, const char *dst_path,
pal_copy_progress_cb_t cb, void *user_data,
uint64_t *cumulative, int *out_errno) {
if ((src_path == NULL) || (dst_path == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (out_errno != NULL) *out_errno = 0;
struct stat st;
if (stat(src_path, &st) < 0)
return fileio_error_from_errno(errno, FTP_ERR_FILE_STAT);
if ((st.st_mode & S_IFMT) != S_IFREG) {
return FTP_ERR_INVALID_PARAM;
}
int src_fd = -1;
int dst_fd = -1;
int tmp_created = 0;
uint8_t *copy_buf = NULL;
size_t copy_buf_size = 0U;
ftp_error_t out_err = FTP_ERR_FILE_WRITE;
char tmp_path[FTP_PATH_MAX] = {0};
src_fd = open(src_path, O_RDONLY);
/* Source pages are one-shot copy data; avoid polluting the page cache where supported. */
if (src_fd >= 0) {
#ifdef F_NOCACHE
(void)fcntl(src_fd, F_NOCACHE, 1);
#endif
#ifdef F_RDAHEAD
(void)fcntl(src_fd, F_RDAHEAD, 1);
#endif
#if defined(POSIX_FADV_SEQUENTIAL) && !defined(PLATFORM_PS4) && !defined(PS4)
(void)posix_fadvise(src_fd, 0, 0, POSIX_FADV_SEQUENTIAL);
#endif
}
if (src_fd < 0) {
int e = errno;
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] open(src) failed: errno=%d path=%.*s", e,
PAL_LOG_PATH_CHARS, src_path);
ftp_log_line(FTP_LOG_WARN, msg);
}
if (out_errno != NULL) {
*out_errno = e;
}
out_err = fileio_error_from_errno(e, FTP_ERR_FILE_OPEN);
goto cleanup;
}
mode_t mode = (mode_t)(st.st_mode & 0777);
/* PFS may report a full filesystem as write()==0, so reject known ENOSPC early. */
{
char dst_dir[FTP_PATH_MAX];
if (fileio_parent_path(dst_path, dst_dir, sizeof(dst_dir)) == FTP_OK) {
struct statvfs vfs;
if (statvfs(dst_dir, &vfs) == 0) {
uint64_t free_bytes =
(uint64_t)vfs.f_bavail * (uint64_t)vfs.f_frsize;
uint64_t need_bytes = (uint64_t)st.st_size;
if (need_bytes > free_bytes) {
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] pre-flight ENOSPC: need=%llu free=%llu dst=%.*s",
(unsigned long long)need_bytes,
(unsigned long long)free_bytes, PAL_LOG_PATH_CHARS,
dst_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) {
*out_errno = ENOSPC;
}
return FTP_ERR_FILE_WRITE;
}
}
}
}
out_err = create_temp_file(dst_path, mode, tmp_path, sizeof(tmp_path),
&dst_fd, out_errno);
if (out_err != FTP_OK) {
if (out_errno != NULL && *out_errno != 0) {
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] create temp failed: errno=%d dst=%.*s", *out_errno,
PAL_LOG_PATH_CHARS, dst_path);
ftp_log_line(FTP_LOG_WARN, msg);
}
goto cleanup;
}
tmp_created = 1;
/* VM-backed buffers avoid fragmenting the daemon buddy allocator. */
if (st.st_size == 0) goto copy_done;
if ((uint64_t)st.st_size >= PAL_COPY_PIPELINE_MIN_BYTES) {
uint8_t *dbuf0 = (uint8_t *)mmap(NULL, PAL_FILE_COPY_BUFFER_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (dbuf0 == MAP_FAILED) { dbuf0 = NULL; }
uint8_t *dbuf1 = (uint8_t *)mmap(NULL, PAL_FILE_COPY_BUFFER_SIZE,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (dbuf1 == MAP_FAILED) { dbuf1 = NULL; }
if ((dbuf0 != NULL) && (dbuf1 != NULL)) {
copy_pipe_t pipe;
pipe.buf[0] = dbuf0;
pipe.buf[1] = dbuf1;
pipe.len[0] = 0U;
pipe.len[1] = 0U;
pipe.fill_idx = 0;
pipe.src_fd = src_fd;
pipe.buf_sz = (size_t)PAL_FILE_COPY_BUFFER_SIZE;
pipe.done = 0;
pipe.reader_err = 0;
pthread_mutex_init(&pipe.mtx, NULL);
pthread_cond_init(&pipe.cv_ready, NULL);
pthread_cond_init(&pipe.cv_free, NULL);
pthread_t reader_tid;
int pt_ret = pthread_create(&reader_tid, NULL, copy_reader_thread, &pipe);
int thread_ok = (pt_ret == 0) ? 1 : 0;
/* Log pthread_create result — on PS4 this can fail with EAGAIN (thread
* limit) or ENOMEM (stack allocation failed under memory pressure). */
if (thread_ok == 0) {
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] pthread_create failed: errno=%d — "
"falling back to serial copy for %.*s",
pt_ret, PAL_LOG_PATH_CHARS, src_path);
ftp_log_line(FTP_LOG_WARN, msg);
}
if (thread_ok != 0) {
ssize_t written = 0; /* last write result — checked after join */
int write_errno = 0; /* saved errno from the last failed write();
* hoisted outside the for loop so it remains
* accessible after break for the post-join log */
pthread_mutex_lock(&pipe.mtx);
for (;;) {
while ((pipe.len[1 - pipe.fill_idx] == 0U) && (pipe.done == 0)) {
pthread_cond_wait(&pipe.cv_ready, &pipe.mtx);
}
int drain_idx = 1 - pipe.fill_idx;
size_t nbytes = pipe.len[drain_idx];
if ((nbytes == 0U) && (pipe.done != 0)) {
break;
}
pthread_mutex_unlock(&pipe.mtx);
/* Keep file-to-file writes large to avoid PFS per-write overhead. */
write_errno = 0;
written = copy_write_full(dst_fd, pipe.buf[drain_idx], nbytes,
&write_errno) == 0
? (ssize_t)nbytes
: -1;
pthread_mutex_lock(&pipe.mtx);
if (written < 0) {
/*
* Write error — signal reader to stop, then break.
* We log below after joining the reader thread.
*/
if (out_errno != NULL) {
*out_errno = write_errno;
}
pipe.done = 1;
pthread_cond_signal(&pipe.cv_free);
out_err = FTP_ERR_FILE_WRITE;
break;
}
if (copy_report_progress((uint64_t)nbytes, cumulative, cb,
user_data) == FTP_ERR_CANCELLED) {
pipe.done = 1;
pthread_cond_signal(&pipe.cv_free);
out_err = FTP_ERR_CANCELLED;
break;
}
pipe.len[drain_idx] = 0U;
pthread_cond_signal(&pipe.cv_free);
}
pthread_mutex_unlock(&pipe.mtx);
(void)pthread_join(reader_tid, NULL);
/* Preserve write errors ahead of reader errors and cancellation. */
if (written < 0) {
char msg[256];
snprintf(msg, sizeof(msg), "[COPY] write failed: errno=%d dst=%.*s",
write_errno, PAL_LOG_PATH_CHARS, dst_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) {
*out_errno = write_errno;
}
out_err = FTP_ERR_FILE_WRITE;
} else if (pipe.reader_err != 0) {
char msg[256];
snprintf(msg, sizeof(msg), "[COPY] read failed: errno=%d src=%.*s",
pipe.reader_err, PAL_LOG_PATH_CHARS, src_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) {
*out_errno = pipe.reader_err;
}
out_err = FTP_ERR_FILE_READ;
} else if (out_err == FTP_ERR_CANCELLED) {
} else {
out_err = FTP_OK;
}
} else {
out_err = FTP_ERR_FILE_WRITE;
}
pthread_mutex_destroy(&pipe.mtx);
pthread_cond_destroy(&pipe.cv_ready);
pthread_cond_destroy(&pipe.cv_free);
(void)munmap(dbuf0, PAL_FILE_COPY_BUFFER_SIZE);
(void)munmap(dbuf1, PAL_FILE_COPY_BUFFER_SIZE);
if (thread_ok != 0) {
if (out_err != FTP_OK) {
goto cleanup;
}
goto copy_done;
}
} else {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] pipeline mmap failed (buf0=%s buf1=%s) — "
"falling back to serial copy for %.*s",
(dbuf0 != NULL) ? "ok" : "NULL",
(dbuf1 != NULL) ? "ok" : "NULL",
PAL_LOG_PATH_CHARS, src_path);
ftp_log_line(FTP_LOG_WARN, msg);
}
if (dbuf0 != NULL) { (void)munmap(dbuf0, PAL_FILE_COPY_BUFFER_SIZE); }
if (dbuf1 != NULL) { (void)munmap(dbuf1, PAL_FILE_COPY_BUFFER_SIZE); }
}
}
copy_buf_size = ((uint64_t)st.st_size < (uint64_t)PAL_FILE_COPY_BUFFER_SIZE)
? (size_t)st.st_size
: (size_t)PAL_FILE_COPY_BUFFER_SIZE;
copy_buf = (uint8_t *)mmap(NULL, copy_buf_size, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (copy_buf == MAP_FAILED) {
copy_buf = NULL;
}
if (copy_buf == NULL) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] serial mmap failed: bufsz=%u errno=%d src=%.*s",
(unsigned)copy_buf_size,
errno,
PAL_LOG_PATH_CHARS, src_path);
ftp_log_line(FTP_LOG_WARN, msg);
}
out_err = FTP_ERR_OUT_OF_MEMORY;
goto cleanup;
}
{
uint64_t serial_written = 0U;
for (;;) {
ssize_t r = read(src_fd, copy_buf, copy_buf_size);
if (r > 0) {
/* The serial fallback uses the same large-write primitive as the pipeline. */
int write_errno = 0;
if (copy_write_full(dst_fd, copy_buf, (size_t)r, &write_errno) != 0) {
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] write failed: errno=%d written_so_far=%llu "
"file_size=%llu dst=%.*s",
write_errno, (unsigned long long)serial_written,
(unsigned long long)st.st_size, PAL_LOG_PATH_CHARS,
dst_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) *out_errno = write_errno;
out_err = FTP_ERR_FILE_WRITE;
goto cleanup;
}
serial_written += (uint64_t)r;
if (copy_report_progress((uint64_t)r, cumulative, cb, user_data) ==
FTP_ERR_CANCELLED) {
out_err = FTP_ERR_CANCELLED;
goto cleanup;
}
continue;
}
if (r == 0) {
break;
}
if (errno == EINTR) {
continue;
}
{
int e = errno;
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] read failed: errno=%d written_so_far=%llu src=%.*s",
e, (unsigned long long)serial_written, PAL_LOG_PATH_CHARS,
src_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) {
*out_errno = e;
}
}
out_err = FTP_ERR_FILE_READ;
goto cleanup;
} /* end serial for(;;) loop */
} /* end serial_written scope */
out_err = FTP_OK;
copy_done:;
/* Evict only clean source pages; DONTNEED on writable PFS fds can force a costly sync. */
#if defined(POSIX_FADV_DONTNEED) && !defined(PLATFORM_PS4) && !defined(PS4)
if (src_fd >= 0) {
(void)posix_fadvise(src_fd, 0, 0, POSIX_FADV_DONTNEED);
}
#endif
if (rename(tmp_path, dst_path) < 0) {
{
int e = errno;
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] rename(tmp->dst) failed: errno=%d tmp=%.*s dst=%.*s",
e, PAL_LOG_PATH_PAIR_CHARS, tmp_path, PAL_LOG_PATH_PAIR_CHARS,
dst_path);
ftp_log_line(FTP_LOG_WARN, msg);
if (out_errno != NULL) {
*out_errno = e;
}
}
out_err = FTP_ERR_FILE_WRITE;
goto cleanup;
}
out_err = FTP_OK;
cleanup:
if (copy_buf != NULL) {
(void)munmap(copy_buf, copy_buf_size);
}
if (dst_fd >= 0) {
(void)close(dst_fd);
}
if (src_fd >= 0) {
(void)close(src_fd);
}
if (out_err != FTP_OK && tmp_created != 0) {
(void)unlink(tmp_path);
}
return out_err;
}
+134
View File
@@ -0,0 +1,134 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file directory.c @brief Directory and path query operations. */
#include "fileio_internal.h"
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/*===========================================================================*
* DIRECTORY OPERATIONS
*===========================================================================*/
ftp_error_t fileio_parent_path(const char *path, char *out, size_t out_size) {
if (path == NULL || out == NULL || out_size == 0U)
return FTP_ERR_INVALID_PARAM;
const char *slash = strrchr(path, '/');
if (slash == NULL) {
if (out_size < 2U) return FTP_ERR_PATH_TOO_LONG;
memcpy(out, ".", 2U);
return FTP_OK;
}
size_t len = (size_t)(slash - path);
if (len == 0U) len = 1U;
if (len >= out_size) return FTP_ERR_PATH_TOO_LONG;
memcpy(out, path, len);
out[len] = '\0';
return FTP_OK;
}
ftp_error_t fileio_join_path(const char *parent, const char *name, char *out,
size_t out_size) {
if (parent == NULL || name == NULL || out == NULL || out_size == 0U)
return FTP_ERR_INVALID_PARAM;
size_t parent_len = strlen(parent);
size_t name_len = strlen(name);
size_t sep = parent_len > 0U && parent[parent_len - 1U] != '/' ? 1U : 0U;
if (parent_len > out_size - 1U || sep > out_size - 1U - parent_len ||
name_len > out_size - 1U - parent_len - sep)
return FTP_ERR_PATH_TOO_LONG;
memcpy(out, parent, parent_len);
size_t pos = parent_len;
if (sep != 0U) out[pos++] = '/';
memcpy(out + pos, name, name_len);
out[pos + name_len] = '\0';
return FTP_OK;
}
static int path_mode(const char *path, mode_t *mode) {
if (path == NULL || mode == NULL) return FTP_ERR_INVALID_PARAM;
struct stat st;
if (stat(path, &st) == 0) {
*mode = st.st_mode;
return 1;
}
if (errno == ENOENT || errno == ENOTDIR) return 0;
return fileio_error_from_errno(errno, FTP_ERR_FILE_STAT);
}
ftp_error_t pal_dir_create(const char *path, mode_t mode) {
if (path == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (mkdir(path, mode) < 0) {
if (errno == EEXIST) return FTP_ERR_DIR_EXISTS;
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
}
return FTP_OK;
}
/**
* @brief Remove directory
*/
ftp_error_t pal_dir_remove(const char *path) {
if (path == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (rmdir(path) < 0) {
if (errno == ENOTEMPTY || errno == ENOTDIR) return FTP_ERR_INVALID_PARAM;
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
}
return FTP_OK;
}
/**
* @brief Check if path exists
*/
int pal_path_exists(const char *path) {
mode_t mode = 0;
return path_mode(path, &mode);
}
int pal_path_is_directory(const char *path) {
mode_t mode = 0;
int result = path_mode(path, &mode);
return result == 1 ? (S_ISDIR(mode) ? 1 : 0) : result;
}
int pal_path_is_file(const char *path) {
mode_t mode = 0;
int result = path_mode(path, &mode);
return result == 1 ? (S_ISREG(mode) ? 1 : 0) : result;
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef ZFTPD_PLATFORM_FILEIO_INTERNAL_H
#define ZFTPD_PLATFORM_FILEIO_INTERNAL_H
#include "pal_fileio.h"
#include <stdint.h>
#define PAL_MOVE_MAX_DEPTH 64U
#define PAL_LOG_PATH_CHARS 120
#define PAL_LOG_PATH_PAIR_CHARS 72
ftp_error_t fileio_error_from_errno(int error, ftp_error_t fallback);
ftp_error_t fileio_parent_path(const char *path, char *out, size_t out_size);
ftp_error_t fileio_join_path(const char *parent, const char *name, char *out,
size_t out_size);
ftp_error_t pal_file_copy_atomic_ex(const char *src_path, const char *dst_path,
pal_copy_progress_cb_t cb, void *user_data,
uint64_t *cumulative, int *out_errno);
#endif
+106
View File
@@ -0,0 +1,106 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file sendfile.c @brief Platform sendfile abstraction. */
#include "pal_fileio.h"
#include "pal_network.h"
#include <errno.h>
#include <unistd.h>
#define FALLBACK_BUFFER_SIZE FTP_BUFFER_SIZE
ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count) {
/* Validate parameters */
if ((sock_fd < 0) || (file_fd < 0)) {
errno = EINVAL;
return -1;
}
if ((offset == NULL) || (*offset < 0)) {
errno = EINVAL;
return -1;
}
if (count == 0U) {
return 0;
}
/* Chunk policy belongs to callers; the PAL forwards the requested count. */
#if defined(__linux__)
return sendfile(sock_fd, file_fd, offset, count);
#elif defined(__APPLE__) && defined(__MACH__)
off_t sent = (off_t)count;
int ret = sendfile(file_fd, sock_fd, *offset, &sent, NULL, 0);
if (sent > 0) *offset += sent;
if (ret == 0 || sent > 0) return sent;
return -1;
#elif defined(__FreeBSD__) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/* FreeBSD uses the inverse fd order and reports partial bytes via sbytes. */
off_t sbytes = 0;
off_t start_offset = *offset;
int ret = sendfile(file_fd, sock_fd, start_offset, count, NULL, &sbytes, 0);
if (sbytes > 0) {
*offset += sbytes;
}
if (ret == 0) {
return sbytes;
} else if ((ret == -1) && (errno == EAGAIN)) {
return sbytes;
} else if ((ret == -1) && (errno == EINTR)) {
/* sbytes already advanced the offset, so expose the partial transfer. */
return sbytes;
} else if ((ret == -1) && pal_file_error_is_fatal(errno)) {
/* Retrying a fatal storage error on an invalid vnode can panic OrbisOS. */
return -1;
} else {
/* Do not hide unsupported sendfile paths behind an implicit copy fallback. */
return (sbytes > 0) ? sbytes : -1;
}
#else
/* Portable fallback for platforms without sendfile. */
static _Thread_local char buffer[FALLBACK_BUFFER_SIZE];
ssize_t nread = pread(
file_fd, buffer,
(count < FALLBACK_BUFFER_SIZE) ? count : FALLBACK_BUFFER_SIZE, *offset);
if (nread <= 0) {
return nread;
}
ssize_t nsent = pal_send_all(sock_fd, buffer, (size_t)nread, 0);
if (nsent < 0) {
return -1;
}
*offset += nsent;
return nsent;
#endif
}
+391
View File
@@ -0,0 +1,391 @@
/*
MIT License
Copyright (c) 2026 Seregon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/** @file tree.c @brief Recursive copy, move and tree removal operations. */
#include "fileio_internal.h"
#include "ftp_log.h"
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* Recursive fallback used when a file operation crosses filesystems. */
static int is_dot_entry(const char *name) {
return name != NULL && name[0] == '.' &&
(name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
}
static int canonicalize_target(const char *path, char *out, size_t out_size) {
if (path == NULL || out == NULL || out_size == 0U) return -1;
if (realpath(path, out) != NULL) return 0;
if (errno != ENOENT) return -1;
const char *slash = strrchr(path, '/');
const char *name = slash != NULL ? slash + 1 : path;
if (name[0] == '\0') return -1;
char parent[FTP_PATH_MAX];
if (fileio_parent_path(path, parent, sizeof(parent)) != FTP_OK) return -1;
char parent_real[FTP_PATH_MAX];
if (realpath(parent, parent_real) == NULL) return -1;
return fileio_join_path(parent_real, name, out, out_size) == FTP_OK ? 0 : -1;
}
static int canonicalize_entry_no_follow(const char *path, char *out,
size_t out_size) {
if (path == NULL || out == NULL || out_size == 0U) return -1;
if (strcmp(path, "/") == 0) {
if (out_size < 2U) return -1;
memcpy(out, "/", 2U);
return 0;
}
const char *slash = strrchr(path, '/');
const char *name = slash != NULL ? slash + 1 : path;
if (name[0] == '\0') return -1;
char parent[FTP_PATH_MAX];
if (fileio_parent_path(path, parent, sizeof(parent)) != FTP_OK) return -1;
char parent_real[FTP_PATH_MAX];
if (realpath(parent, parent_real) == NULL) return -1;
return fileio_join_path(parent_real, name, out, out_size) == FTP_OK ? 0 : -1;
}
static int path_is_same_or_child(const char *path, const char *root) {
size_t root_len = strlen(root);
if (strcmp(path, root) == 0) return 1;
if (root_len == 1U && root[0] == '/') return path[0] == '/';
return root_len > 0U && strncmp(path, root, root_len) == 0 &&
path[root_len] == '/';
}
static ftp_error_t validate_copy_roots(const char *src, const char *dst) {
char src_real[FTP_PATH_MAX];
char dst_real[FTP_PATH_MAX];
struct stat st;
if (lstat(src, &st) < 0) return fileio_error_from_errno(errno, FTP_ERR_FILE_STAT);
int src_ok = S_ISLNK(st.st_mode)
? canonicalize_entry_no_follow(src, src_real, sizeof(src_real))
: (realpath(src, src_real) != NULL ? 0 : -1);
if (src_ok != 0 || canonicalize_target(dst, dst_real, sizeof(dst_real)) != 0)
return FTP_ERR_PATH_INVALID;
if (path_is_same_or_child(dst_real, src_real)) return FTP_ERR_INVALID_PARAM;
return FTP_OK;
}
static ftp_error_t pal_dir_remove_recursive(const char *path, unsigned depth) {
if (path == NULL) {
return FTP_ERR_INVALID_PARAM;
}
if (depth > PAL_MOVE_MAX_DEPTH) {
return FTP_ERR_PATH_TOO_LONG;
}
DIR *dir = opendir(path);
if (dir == NULL) {
if (errno == ENOENT) {
return FTP_OK;
}
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] opendir(cleanup) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, path);
ftp_log_line(FTP_LOG_WARN, msg);
}
return FTP_ERR_DIR_OPEN;
}
struct dirent *ent;
ftp_error_t err = FTP_OK;
while ((ent = readdir(dir)) != NULL) {
if (is_dot_entry(ent->d_name)) continue;
char child[FTP_PATH_MAX];
err = fileio_join_path(path, ent->d_name, child, sizeof(child));
if (err != FTP_OK) break;
struct stat st;
if (lstat(child, &st) < 0) {
err = fileio_error_from_errno(errno, FTP_ERR_FILE_STAT);
break;
}
if (S_ISDIR(st.st_mode)) {
err = pal_dir_remove_recursive(child, depth + 1U);
} else {
if (unlink(child) != 0) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] unlink(cleanup) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, child);
ftp_log_line(FTP_LOG_WARN, msg);
}
err = FTP_ERR_FILE_WRITE;
}
}
if (err != FTP_OK) {
break;
}
}
(void)closedir(dir);
if (err == FTP_OK) {
if (rmdir(path) < 0) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] rmdir(cleanup) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, path);
ftp_log_line(FTP_LOG_WARN, msg);
}
err = FTP_ERR_FILE_WRITE;
}
}
return err;
}
static ftp_error_t pal_copy_cross_device_r_ex(const char *src, const char *dst,
unsigned depth, int keep_src,
pal_copy_progress_cb_t cb,
void *user_data,
uint64_t *cumulative,
int *out_errno) {
if ((src == NULL) || (dst == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (depth > PAL_MOVE_MAX_DEPTH) {
{
char msg[256];
snprintf(msg, sizeof(msg), "[XDEV] max depth %u exceeded: %.*s",
(unsigned)PAL_MOVE_MAX_DEPTH, PAL_LOG_PATH_CHARS, src);
ftp_log_line(FTP_LOG_WARN, msg);
}
return FTP_ERR_PATH_TOO_LONG;
}
if (depth == 0U) {
ftp_error_t root_err = validate_copy_roots(src, dst);
if (root_err != FTP_OK) return root_err;
char msg[256];
snprintf(msg, sizeof(msg), "[XDEV] cross-device move: %.*s -> %.*s",
PAL_LOG_PATH_PAIR_CHARS, src, PAL_LOG_PATH_PAIR_CHARS, dst);
ftp_log_line(FTP_LOG_INFO, msg);
}
struct stat src_st;
if (lstat(src, &src_st) < 0) {
int e = errno;
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] stat(src) failed: errno=%d path=%.*s", e,
PAL_LOG_PATH_CHARS, src);
ftp_log_line(FTP_LOG_WARN, msg);
}
return (e == ENOENT) ? FTP_ERR_NOT_FOUND : FTP_ERR_FILE_STAT;
}
if (S_ISLNK(src_st.st_mode)) {
char target[FTP_PATH_MAX];
ssize_t n = readlink(src, target, sizeof(target) - 1U);
if (n < 0) return fileio_error_from_errno(errno, FTP_ERR_FILE_READ);
target[n] = '\0';
if (symlink(target, dst) < 0)
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
if (keep_src == 0 && unlink(src) < 0)
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
return FTP_OK;
}
if (S_ISREG(src_st.st_mode)) {
int local_errno = 0;
int *errno_ptr = (out_errno != NULL) ? out_errno : &local_errno;
ftp_error_t err =
pal_file_copy_atomic_ex(src, dst, cb, user_data, cumulative, errno_ptr);
if (err != FTP_OK) {
{
int os_err = *errno_ptr;
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] file copy failed (err=%d, errno=%d): %.*s -> %.*s",
(int)err, os_err, PAL_LOG_PATH_PAIR_CHARS, src,
PAL_LOG_PATH_PAIR_CHARS, dst);
ftp_log_line(FTP_LOG_WARN, msg);
}
return err;
}
if (keep_src == 0) {
if (unlink(src) < 0) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] unlink(src) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, src);
ftp_log_line(FTP_LOG_WARN, msg);
}
return FTP_ERR_FILE_WRITE;
}
}
return FTP_OK;
}
if (!S_ISDIR(src_st.st_mode)) {
return FTP_ERR_INVALID_PARAM;
}
mode_t mode = (mode_t)(src_st.st_mode & 0777);
/* Never roll back a destination directory that pre-dated this operation. */
int dst_created_by_us = 0;
if (mkdir(dst, mode) < 0) {
if (errno != EEXIST) {
int e = errno;
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] mkdir(dst) failed: errno=%d path=%.*s", e,
PAL_LOG_PATH_CHARS, dst);
ftp_log_line(FTP_LOG_WARN, msg);
}
return FTP_ERR_FILE_WRITE;
}
} else {
dst_created_by_us = 1;
}
DIR *dir = opendir(src);
if (dir == NULL) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] opendir(src) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, src);
ftp_log_line(FTP_LOG_WARN, msg);
}
if (dst_created_by_us != 0) {
(void)rmdir(dst);
}
return FTP_ERR_DIR_OPEN;
}
struct dirent *ent;
ftp_error_t err = FTP_OK;
while ((ent = readdir(dir)) != NULL) {
if (is_dot_entry(ent->d_name)) continue;
char src_child[FTP_PATH_MAX];
char dst_child[FTP_PATH_MAX];
err = fileio_join_path(src, ent->d_name, src_child, sizeof(src_child));
if (err == FTP_OK)
err = fileio_join_path(dst, ent->d_name, dst_child, sizeof(dst_child));
if (err != FTP_OK) break;
err = pal_copy_cross_device_r_ex(src_child, dst_child, depth + 1U, keep_src,
cb, user_data, cumulative, out_errno);
if (err != FTP_OK) {
break;
}
}
(void)closedir(dir);
if (err != FTP_OK) {
if (dst_created_by_us != 0) {
(void)pal_dir_remove_recursive(dst, 0U);
}
return err;
}
if (keep_src == 0) {
if (rmdir(src) < 0) {
{
char msg[256];
snprintf(msg, sizeof(msg),
"[XDEV] rmdir(src) failed: errno=%d path=%.*s", errno,
PAL_LOG_PATH_CHARS, src);
ftp_log_line(FTP_LOG_WARN, msg);
}
return FTP_ERR_FILE_WRITE;
}
}
if (depth == 0U) {
ftp_log_line(FTP_LOG_INFO, "[XDEV] cross-device operation completed OK");
}
return FTP_OK;
}
ftp_error_t pal_file_rename(const char *old_path, const char *new_path) {
if ((old_path == NULL) || (new_path == NULL)) {
return FTP_ERR_INVALID_PARAM;
}
if (rename(old_path, new_path) < 0) {
if (errno == EXDEV) return FTP_ERR_CROSS_DEVICE;
return fileio_error_from_errno(errno, FTP_ERR_FILE_WRITE);
}
return FTP_OK;
}
ftp_error_t pal_file_copy_recursive(const char *src, const char *dst,
int keep_src) {
uint64_t cum = 0U;
return pal_copy_cross_device_r_ex(src, dst, 0U, keep_src, NULL, NULL, &cum,
NULL);
}
ftp_error_t pal_file_copy_recursive_ex(const char *src, const char *dst,
int keep_src, pal_copy_progress_cb_t cb,
void *user_data, int *out_errno) {
uint64_t cum = 0U;
return pal_copy_cross_device_r_ex(src, dst, 0U, keep_src, cb, user_data, &cum,
out_errno);
}
ftp_error_t pal_dir_remove_recursive_pub(const char *path) {
if (path == NULL || path[0] == '\0') return FTP_ERR_INVALID_PARAM;
char resolved[FTP_PATH_MAX];
if (realpath(path, resolved) != NULL && strcmp(resolved, "/") == 0)
return FTP_ERR_PERMISSION;
if (strcmp(path, "/") == 0) return FTP_ERR_PERMISSION;
return pal_dir_remove_recursive(path, 0U);
}
File diff suppressed because it is too large. Load diff
+1 -42
View File
@@ -109,9 +109,6 @@ ftp_error_t vfs_open(vfs_node_t *node, const char *path)
* available for platform-internal use (e.g. module loading) but must not
* be on the data-transfer path.
*
* @note VFS_CAP_SENDFILE is now always set on PS4/PS5 — pal_sendfile()
* handles unsupported filesystems with a transparent
* pread()+send_all() fallback when the kernel returns EINVAL.
*/
int fd = pal_file_open(path, O_RDONLY, 0);
@@ -130,46 +127,8 @@ ftp_error_t vfs_open(vfs_node_t *node, const char *path)
node->size = (uint64_t)st.st_size;
node->offset = 0U;
/*
* SENDFILE SAFETY CHECK (PS4/PS5)
*
* DESIGN RATIONALE:
* FreeBSD's sendfile(2) uses the kernel VM pager to DMA file pages
* directly into the socket buffer (zero userspace copy). This requires
* the source vnode's pager to implement the vm_pager_ops interface.
*
* On PS5's modified FreeBSD kernel, the exFAT (exfatfs) and FAT32
* (msdosfs) drivers used for USB storage do NOT implement this
* interface correctly. Calling sendfile() on a vnode backed by these
* filesystems dereferences a null/invalid pager function pointer,
* causing an immediate kernel panic (KP).
*
* nullfs mirrors the underlying vnode — if the origin is exFAT, the
* nullfs vnode inherits the same broken pager ops.
*
* Detection: fstatfs() on the open fd returns the filesystem type
* name without any additional syscall cost. For USB-backed filesystems
* we clear VFS_CAP_SENDFILE, forcing the buffered read/write path.
*
* @see pal_sendfile() in pal_fileio.c — callers must check this cap
* @see https://github.com/seregonwar/zftpd — KP report from USB download
*/
#if defined(PLATFORM_PS4) || defined(PLATFORM_PS5)
/*
* SENDFILE — enabled on all PS4/PS5 filesystems.
*
* The previous blacklist (exfatfs, msdosfs, nullfs, pfsmnt, pfs)
* disabled VFS_CAP_SENDFILE for most actual filesystems
* on PS5, forcing the read()+send() path for FTP RETR.
*
* Zero-copy sendfile is the only transfer path. If the kernel
* cannot DMA from a particular filesystem, the transfer fails
* with 426 — there is no silent userspace fallback.
*/
/* Transfer backends operate on raw file bytes; capability is handled by pal_sendfile(). */
node->caps = VFS_CAP_SENDFILE;
#else
node->caps = VFS_CAP_SENDFILE;
#endif
return FTP_OK;
}
+124
View File
@@ -0,0 +1,124 @@
#include "pal_fileio.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#define CHECK(x) do { \
if (!(x)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \
return -1; \
} \
} while (0)
static int make_tcp_pair(int *server_fd, int *client_fd) {
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) return -1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = 0;
if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) != 0 ||
listen(listener, 1) != 0) {
close(listener);
return -1;
}
socklen_t len = sizeof(addr);
if (getsockname(listener, (struct sockaddr *)&addr, &len) != 0) {
close(listener);
return -1;
}
int client = socket(AF_INET, SOCK_STREAM, 0);
if (client < 0 || connect(client, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
if (client >= 0) close(client);
close(listener);
return -1;
}
int server = accept(listener, NULL, NULL);
close(listener);
if (server < 0) { close(client); return -1; }
*server_fd = server;
*client_fd = client;
return 0;
}
static int test_basic_io(const char *path) {
int fd = pal_file_open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
CHECK(fd >= 0);
CHECK(pal_file_write(fd, NULL, 0U) == 0);
CHECK(pal_file_write_all(fd, NULL, 0U) == 0);
static const char payload[] = "abcdef";
CHECK(pal_file_write_all(fd, payload, sizeof(payload) - 1U) ==
(ssize_t)(sizeof(payload) - 1U));
CHECK(pal_file_seek(fd, 0, SEEK_SET) == 0);
CHECK(pal_file_read(fd, NULL, 0U) == 0);
char buf[8] = {0};
CHECK(pal_file_read(fd, buf, 6U) == 6);
CHECK(memcmp(buf, payload, 6U) == 0);
CHECK(pal_file_truncate(fd, 3) == FTP_OK);
struct stat st;
CHECK(pal_file_fstat(fd, &st) == FTP_OK);
CHECK(st.st_size == 3);
CHECK(pal_file_close(fd) == FTP_OK);
return 0;
}
static int test_sendfile_path(const char *path) {
int file_fd = pal_file_open(path, O_RDONLY, 0);
CHECK(file_fd >= 0);
int server_fd = -1, client_fd = -1;
CHECK(make_tcp_pair(&server_fd, &client_fd) == 0);
off_t offset = 0;
CHECK(pal_sendfile(server_fd, file_fd, &offset, 0U) == 0);
size_t remaining = 3U;
while (remaining > 0U) {
ssize_t n = pal_sendfile(server_fd, file_fd, &offset, remaining);
CHECK(n > 0);
CHECK((size_t)n <= remaining);
remaining -= (size_t)n;
}
CHECK(offset == 3);
char buf[4] = {0};
size_t got = 0U;
while (got < 3U) {
ssize_t n = recv(client_fd, buf + got, 3U - got, 0);
CHECK(n > 0);
got += (size_t)n;
}
CHECK(memcmp(buf, "abc", 3U) == 0);
errno = 0;
CHECK(pal_sendfile(server_fd, file_fd, NULL, 1U) == -1);
CHECK(errno == EINVAL);
CHECK(pal_file_close(file_fd) == FTP_OK);
close(server_fd);
close(client_fd);
return 0;
}
int main(void) {
char path[] = "/tmp/zftpd-fileio-basic-XXXXXX";
int seed_fd = mkstemp(path);
CHECK(seed_fd >= 0);
close(seed_fd);
CHECK(test_basic_io(path) == 0);
CHECK(test_sendfile_path(path) == 0);
CHECK(pal_file_stat(path, &(struct stat){0}) == FTP_OK);
CHECK(pal_file_delete(path) == FTP_OK);
CHECK(pal_file_stat(path, &(struct stat){0}) == FTP_ERR_NOT_FOUND);
CHECK(pal_path_exists(path) == 0);
CHECK(pal_path_is_file(path) == 0);
CHECK(pal_path_is_directory(path) == 0);
puts("test_fileio_basic: ok");
return 0;
}
+224
View File
@@ -0,0 +1,224 @@
#include "pal_fileio.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define CHECK(x) do { \
if (!(x)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #x); \
return -1; \
} \
} while (0)
static int write_text(const char *path, const char *text) {
FILE *fp = fopen(path, "wb");
if (fp == NULL) return -1;
size_t len = strlen(text);
int ok = fwrite(text, 1U, len, fp) == len;
fclose(fp);
return ok ? 0 : -1;
}
static int file_equals(const char *path, const char *expected) {
char buf[128];
FILE *fp = fopen(path, "rb");
if (fp == NULL) return 0;
size_t n = fread(buf, 1U, sizeof(buf) - 1U, fp);
fclose(fp);
buf[n] = '\0';
return strcmp(buf, expected) == 0;
}
static int write_large_file(const char *path, size_t size) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd < 0) return -1;
unsigned char buf[65536];
memset(buf, 0xA5, sizeof(buf));
size_t left = size;
while (left > 0U) {
size_t n = left < sizeof(buf) ? left : sizeof(buf);
if (write(fd, buf, n) != (ssize_t)n) { close(fd); return -1; }
left -= n;
}
return close(fd);
}
static int cancel_copy(uint64_t bytes, void *user_data) {
int *calls = (int *)user_data;
(void)bytes;
(*calls)++;
return -1;
}
static int test_temp_collision(const char *root) {
char src[FTP_PATH_MAX], dst[FTP_PATH_MAX], stale[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/collision-src", root) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/collision-dst", root) > 0);
CHECK(snprintf(stale, sizeof(stale), "%s/.zftpd.%lu.0.tmp", root,
(unsigned long)getpid()) > 0);
CHECK(write_text(src, "payload") == 0);
CHECK(write_text(stale, "sentinel") == 0);
CHECK(pal_file_copy_recursive(src, dst, 1) == FTP_OK);
CHECK(file_equals(dst, "payload"));
CHECK(file_equals(stale, "sentinel"));
CHECK(unlink(src) == 0);
CHECK(unlink(dst) == 0);
CHECK(unlink(stale) == 0);
return 0;
}
static int test_recursive_copy(const char *root) {
char src[FTP_PATH_MAX], nested[FTP_PATH_MAX], file[FTP_PATH_MAX];
char dst[FTP_PATH_MAX], copied[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/src", root) > 0);
CHECK(snprintf(nested, sizeof(nested), "%s/nested", src) > 0);
CHECK(snprintf(file, sizeof(file), "%s/value.txt", nested) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/dst", root) > 0);
CHECK(snprintf(copied, sizeof(copied), "%s/nested/value.txt", dst) > 0);
CHECK(mkdir(src, 0700) == 0);
CHECK(mkdir(nested, 0700) == 0);
CHECK(write_text(file, "tree-copy") == 0);
CHECK(pal_file_copy_recursive(src, dst, 1) == FTP_OK);
CHECK(file_equals(file, "tree-copy"));
CHECK(file_equals(copied, "tree-copy"));
return 0;
}
static int test_invalid_copy_roots(const char *root) {
char src[FTP_PATH_MAX], child[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/src", root) > 0);
CHECK(snprintf(child, sizeof(child), "%s/inside-copy", src) > 0);
CHECK(pal_file_copy_recursive(src, src, 1) == FTP_ERR_INVALID_PARAM);
CHECK(pal_file_copy_recursive(src, child, 1) == FTP_ERR_INVALID_PARAM);
CHECK(pal_path_exists(child) == 0);
CHECK(pal_file_copy_recursive("/", child, 1) == FTP_ERR_INVALID_PARAM);
return 0;
}
static int count_copy_temps(const char *dir_path) {
DIR *dir = opendir(dir_path);
if (dir == NULL) return -1;
int count = 0;
struct dirent *ent;
while ((ent = readdir(dir)) != NULL) {
if (strncmp(ent->d_name, ".zftpd.", 7U) == 0) count++;
}
closedir(dir);
return count;
}
static int test_cancelled_copy(const char *root) {
char src[FTP_PATH_MAX], dst[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/cancel-src.bin", root) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/cancel-dst.bin", root) > 0);
CHECK(write_large_file(src, 6U * 1024U * 1024U) == 0);
int calls = 0;
int os_errno = 0;
ftp_error_t err = pal_file_copy_recursive_ex(src, dst, 1, cancel_copy,
&calls, &os_errno);
CHECK(err == FTP_ERR_CANCELLED);
CHECK(calls == 1);
CHECK(pal_path_exists(src) == 1);
CHECK(pal_path_exists(dst) == 0);
CHECK(count_copy_temps(root) == 0);
CHECK(unlink(src) == 0);
return 0;
}
static int test_special_file_rejected(const char *root) {
char fifo_path[FTP_PATH_MAX], dst[FTP_PATH_MAX];
CHECK(snprintf(fifo_path, sizeof(fifo_path), "%s/input.fifo", root) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/fifo-copy", root) > 0);
CHECK(mkfifo(fifo_path, 0600) == 0);
CHECK(pal_file_copy_recursive(fifo_path, dst, 1) == FTP_ERR_INVALID_PARAM);
CHECK(pal_path_exists(dst) == 0);
CHECK(unlink(fifo_path) == 0);
return 0;
}
static int test_symlink_copy(const char *root) {
char src[FTP_PATH_MAX], dst[FTP_PATH_MAX], link_src[FTP_PATH_MAX];
char link_dst[FTP_PATH_MAX], external[FTP_PATH_MAX], target[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/link-src", root) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/link-dst", root) > 0);
CHECK(snprintf(link_src, sizeof(link_src), "%s/external-link", src) > 0);
CHECK(snprintf(link_dst, sizeof(link_dst), "%s/external-link", dst) > 0);
CHECK(snprintf(external, sizeof(external), "%s/external.txt", root) > 0);
CHECK(mkdir(src, 0700) == 0);
CHECK(write_text(external, "outside") == 0);
CHECK(symlink("../external.txt", link_src) == 0);
CHECK(pal_file_copy_recursive(src, dst, 1) == FTP_OK);
struct stat st;
CHECK(lstat(link_dst, &st) == 0 && S_ISLNK(st.st_mode));
ssize_t n = readlink(link_dst, target, sizeof(target) - 1U);
CHECK(n > 0);
target[n] = '\0';
CHECK(strcmp(target, "../external.txt") == 0);
CHECK(file_equals(external, "outside"));
CHECK(pal_dir_remove_recursive_pub(src) == FTP_OK);
CHECK(pal_dir_remove_recursive_pub(dst) == FTP_OK);
CHECK(unlink(external) == 0);
return 0;
}
static int test_delete_does_not_follow_symlink(const char *root) {
char tree[FTP_PATH_MAX], external[FTP_PATH_MAX], sentinel[FTP_PATH_MAX];
char link_path[FTP_PATH_MAX];
CHECK(snprintf(tree, sizeof(tree), "%s/delete-link-tree", root) > 0);
CHECK(snprintf(external, sizeof(external), "%s/external-dir", root) > 0);
CHECK(snprintf(sentinel, sizeof(sentinel), "%s/sentinel.txt", external) > 0);
CHECK(snprintf(link_path, sizeof(link_path), "%s/outside", tree) > 0);
CHECK(mkdir(tree, 0700) == 0);
CHECK(mkdir(external, 0700) == 0);
CHECK(write_text(sentinel, "keep-me") == 0);
CHECK(symlink(external, link_path) == 0);
CHECK(pal_dir_remove_recursive_pub(tree) == FTP_OK);
CHECK(pal_path_exists(tree) == 0);
CHECK(file_equals(sentinel, "keep-me"));
CHECK(unlink(sentinel) == 0);
CHECK(rmdir(external) == 0);
return 0;
}
static int test_recursive_delete(const char *root) {
char tree[FTP_PATH_MAX], nested[FTP_PATH_MAX], file[FTP_PATH_MAX];
CHECK(snprintf(tree, sizeof(tree), "%s/delete-tree", root) > 0);
CHECK(snprintf(nested, sizeof(nested), "%s/nested", tree) > 0);
CHECK(snprintf(file, sizeof(file), "%s/value.txt", nested) > 0);
CHECK(mkdir(tree, 0700) == 0);
CHECK(mkdir(nested, 0700) == 0);
CHECK(write_text(file, "remove-me") == 0);
CHECK(pal_dir_remove_recursive_pub("/") == FTP_ERR_PERMISSION);
CHECK(pal_dir_remove_recursive_pub("/./") == FTP_ERR_PERMISSION);
CHECK(pal_dir_remove_recursive_pub(tree) == FTP_OK);
CHECK(pal_path_exists(tree) == 0);
return 0;
}
int main(void) {
char root[] = "/tmp/zftpd-fileio-tree-XXXXXX";
CHECK(mkdtemp(root) != NULL);
CHECK(test_temp_collision(root) == 0);
CHECK(test_recursive_copy(root) == 0);
CHECK(test_invalid_copy_roots(root) == 0);
CHECK(test_cancelled_copy(root) == 0);
CHECK(test_special_file_rejected(root) == 0);
CHECK(test_symlink_copy(root) == 0);
CHECK(test_delete_does_not_follow_symlink(root) == 0);
CHECK(test_recursive_delete(root) == 0);
char src[FTP_PATH_MAX], dst[FTP_PATH_MAX];
CHECK(snprintf(src, sizeof(src), "%s/src", root) > 0);
CHECK(snprintf(dst, sizeof(dst), "%s/dst", root) > 0);
CHECK(pal_dir_remove_recursive_pub(src) == FTP_OK);
CHECK(pal_dir_remove_recursive_pub(dst) == FTP_OK);
CHECK(rmdir(root) == 0);
puts("test_fileio_tree: ok");
return 0;
}