mirror of
https://github.com/seregonwar/zftpd.git
synced 2026-10-19 01:59:57 -05:00
refactor: modularize and harden file I/O
This commit is contained in:
1 parent
a62b2ad2b2
commit
a042eae480
14 files changed
+1847
-2090
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user