From fb1646ce2bec8d04fbcaf651dad086cfdfd4322f Mon Sep 17 00:00:00 2001 From: seregonwar <109359355+seregonwar@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:50:45 +0200 Subject: [PATCH] refactor(io): shared file I/O layer and FTP instance plumbing Move copy, sendfile and directory helpers behind one platform layer, let the FTP server run beside the HTTP stack with its own instance bookkeeping, and keep the transfer command on the shared pipeline. --- include/ftp/ftp_config.h | 59 ++---------------- include/ftp/ftp_instance.h | 27 +++++++++ include/platform/pal_fileio.h | 3 + src/ftp/commands/transfer.c | 99 +++++++------------------------ src/ftp/ftp_instance.c | 102 ++++++++++++++++++++++++++++++++ src/platform/fileio/basic.c | 15 +---- src/platform/fileio/copy.c | 2 - src/platform/fileio/directory.c | 26 ++++++++ src/platform/fileio/sendfile.c | 17 ++++++ tests/test_fileio_basic.c | 27 +++++++++ tests/test_instance.c | 43 ++++++++++++++ 11 files changed, 272 insertions(+), 148 deletions(-) diff --git a/include/ftp/ftp_config.h b/include/ftp/ftp_config.h index 908835c..8ca3cbd 100644 --- a/include/ftp/ftp_config.h +++ b/include/ftp/ftp_config.h @@ -43,7 +43,7 @@ SOFTWARE. *===========================================================================*/ #ifndef RELEASE_VERSION -#define RELEASE_VERSION "1.5.0" +#define RELEASE_VERSION "1.6.0" #endif /*===========================================================================* @@ -180,34 +180,10 @@ SOFTWARE. #define FTP_LIST_LINE_SIZE 512U #endif -/*===========================================================================* - * PATH LIMITS (Platform-dependent) - *===========================================================================*/ - -#if defined(PS4) || defined(PS5) || defined(PLATFORM_PS4) || defined(PLATFORM_PS5) -/** - * PlayStation maximum path length - * @note PS4/PS5 use custom BSD with 1024-byte limit - */ +/* Filesystem path capacity is platform-owned; keep the FTP name as an ABI alias. */ +#include "pal_limits.h" #ifndef FTP_PATH_MAX -#define FTP_PATH_MAX 1024U -#endif -#elif defined(PS3) -/** - * PlayStation 3 maximum path length - * @note PS3 has more limited path support - */ -#ifndef FTP_PATH_MAX -#define FTP_PATH_MAX 512U -#endif -#else -/** - * POSIX maximum path length - * @note Linux typically supports 4096 bytes - */ -#ifndef FTP_PATH_MAX -#define FTP_PATH_MAX 4096U -#endif +#define FTP_PATH_MAX PAL_PATH_MAX #endif /** @@ -374,32 +350,9 @@ SOFTWARE. #define FTP_SENDFILE_EAGAIN_SLEEP_US 500U /* 0.5 ms — PS5 LAN RTT ~0.1-0.3ms, 1ms era eccessivo */ #endif -/** - * DESIGN RATIONALE - distinct from PAL_FILE_WRITE_CHUNK_MAX: - * - * PAL_FILE_WRITE_CHUNK_MAX (64 KB on PS4, 128 KB on PS5) was designed - * to keep per-chunk PFS WRITE latency under ~5 ms in the STOR - * double-buffer path so the FTP recv thread never starves the TCP - * receive window. It is a WRITE latency constraint, not a READ one. - * - * Using it in cmd_RETR sendfile() penalises downloads with 4-8x the - * number of syscalls compared to the HTTP server (512 KB chunks). - * Each unnecessary syscall boundary: - * - re-acquires the socket lock - * - may flush a partial TCP_CORK/TCP_NOPUSH window prematurely - * - triggers a false "driver stall" if the send buffer is momentarily - * full (TCP back-pressure) before sufficient ACKs have arrived - * - * 512 KB is: - * - below the PS5 sendfile EAGAIN threshold (>= 1 MB) - * - large enough that one chunk fills the BDP on most internet paths - * - the value HTTP /api/download already uses (HTTP_SENDFILE_CHUNK_SIZE) - * - * @see cmd_RETR in ftp_commands.c - * @see HTTP_SENDFILE_CHUNK_SIZE in http_config.h - */ +/** FTP RETR sendfile chunk size. */ #ifndef FTP_RETR_SENDFILE_CHUNK -#define FTP_RETR_SENDFILE_CHUNK (2U * 1024U * 1024U) /* 2 MB — PS5 NVMe: meno syscall boundary, meno TCP flush prematuri */ +#define FTP_RETR_SENDFILE_CHUNK (2U * 1024U * 1024U) #endif /** diff --git a/include/ftp/ftp_instance.h b/include/ftp/ftp_instance.h index b3c1330..734492e 100644 --- a/include/ftp/ftp_instance.h +++ b/include/ftp/ftp_instance.h @@ -33,6 +33,7 @@ SOFTWARE. #ifndef FTP_INSTANCE_H #define FTP_INSTANCE_H +#include #include #ifdef __cplusplus @@ -55,6 +56,32 @@ uint64_t ftp_daemon_instance_id(void); */ uint64_t ftp_daemon_start_monotonic_ns(void); +/** + * @brief Format the control identity line (" \n"). + * @return 0 on success, -1 when the buffer cannot hold the line. + */ +int ftp_instance_identity_format(char *out, size_t capacity, int pid, + uint64_t token); + +/** + * @brief Parse a control identity line written by the formatter. + * @return 0 when the line is complete and the pid is usable, -1 otherwise. + */ +int ftp_instance_identity_parse(const char *text, int *pid, uint64_t *token); + +/** + * @brief Format the cooperative shutdown request ("STOP \n"). + * @return 0 on success, -1 when the buffer cannot hold the command. + */ +int ftp_instance_stop_command_format(char *out, size_t capacity, + uint64_t token); + +/** + * @brief Parse a cooperative shutdown request. + * @return 0 when the line is exactly the command, -1 otherwise. + */ +int ftp_instance_stop_command_parse(const char *line, uint64_t *token); + #ifdef __cplusplus } #endif diff --git a/include/platform/pal_fileio.h b/include/platform/pal_fileio.h index b5751ca..0540479 100644 --- a/include/platform/pal_fileio.h +++ b/include/platform/pal_fileio.h @@ -68,6 +68,8 @@ SOFTWARE. * Callers must handle partial transfers and EAGAIN. */ ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count); +ssize_t pal_sendfile_retry(int sock_fd, int file_fd, off_t *offset, size_t count, + unsigned retries, unsigned sleep_us); int pal_file_error_is_fatal(int error); /*===========================================================================* @@ -277,6 +279,7 @@ ftp_error_t pal_file_copy_recursive_ex(const char *src, const char *dst, * @pre path != NULL */ ftp_error_t pal_dir_create(const char *path, mode_t mode); +ftp_error_t pal_dir_create_recursive(const char *path, mode_t mode); /** * @brief Remove directory diff --git a/src/ftp/commands/transfer.c b/src/ftp/commands/transfer.c index 341c1a4..5957752 100644 --- a/src/ftp/commands/transfer.c +++ b/src/ftp/commands/transfer.c @@ -189,9 +189,9 @@ static void upload_sync_file(int fd, int success) { (void)fd; (void)success; #elif defined(__linux__) - if (success != 0) (void)fdatasync(fd); + if (success != 0 && fd >= 0) (void)fdatasync(fd); #else - if (success != 0) (void)fsync(fd); + if (success != 0 && fd >= 0) (void)fsync(fd); #endif } @@ -326,83 +326,23 @@ ftp_error_t cmd_RETR(ftp_session_t *session, const char *args) { pal_socket_cork(session->data_fd); while (remaining > 0U) { - ssize_t sent = - pal_sendfile(session->data_fd, node.fd, &offset, - (remaining > (size_t)FTP_RETR_SENDFILE_CHUNK) - ? (size_t)FTP_RETR_SENDFILE_CHUNK - : remaining); + size_t chunk = remaining > (size_t)FTP_RETR_SENDFILE_CHUNK + ? (size_t)FTP_RETR_SENDFILE_CHUNK + : remaining; + ssize_t sent = pal_sendfile_retry( + session->data_fd, node.fd, &offset, chunk, + FTP_SENDFILE_EAGAIN_RETRIES, FTP_SENDFILE_EAGAIN_SLEEP_US); + if (sent <= 0) break; - if (sent > 0) { - remaining -= (size_t)sent; - bytes_sent += (uint64_t)sent; - session->last_activity = time(NULL); - atomic_fetch_add(&session->stats.bytes_sent, (uint64_t)sent); - - /* Evict pages already sent from the kernel page cache. */ + remaining -= (size_t)sent; + bytes_sent += (uint64_t)sent; + session->last_activity = time(NULL); + atomic_fetch_add(&session->stats.bytes_sent, (uint64_t)sent); #if defined(POSIX_FADV_DONTNEED) && !defined(PLATFORM_PS4) && !defined(PS4) - if (node.fd >= 0) { - off_t evict_start = offset - (off_t)sent; - if (evict_start >= 0) { - (void)posix_fadvise(node.fd, evict_start, - (off_t)sent, POSIX_FADV_DONTNEED); - } - } + off_t evict_start = offset - (off_t)sent; + if (node.fd >= 0 && evict_start >= 0) + (void)posix_fadvise(node.fd, evict_start, (off_t)sent, POSIX_FADV_DONTNEED); #endif - continue; - } - - if ((sent < 0) && (errno == EINTR)) { - continue; - } - - /* Retrying a bad vnode can panic PS4/PS5, so storage faults are terminal. */ - if ((sent < 0) && pal_file_error_is_fatal(errno)) { - remaining = 1U; - break; - } - - /* Retry bounded TCP back-pressure/driver stalls. */ - { - int recovered = 0; - for (int r = 0; r < FTP_SENDFILE_EAGAIN_RETRIES; r++) { - usleep(FTP_SENDFILE_EAGAIN_SLEEP_US); - ssize_t r_sent = - pal_sendfile(session->data_fd, node.fd, &offset, - (remaining > (size_t)FTP_RETR_SENDFILE_CHUNK) - ? (size_t)FTP_RETR_SENDFILE_CHUNK - : remaining); - if (r_sent > 0) { - remaining -= (size_t)r_sent; - bytes_sent += (uint64_t)r_sent; - session->last_activity = time(NULL); - atomic_fetch_add(&session->stats.bytes_sent, (uint64_t)r_sent); -#if defined(POSIX_FADV_DONTNEED) && !defined(PLATFORM_PS4) && !defined(PS4) - if (node.fd >= 0) { - off_t evict_start = offset - (off_t)r_sent; - if (evict_start >= 0) { - (void)posix_fadvise(node.fd, evict_start, - (off_t)r_sent, POSIX_FADV_DONTNEED); - } - } -#endif - recovered = 1; - break; - } - if ((r_sent < 0) && (errno == EINTR)) { - r--; /* don't count EINTR as a retry */ - } - if ((r_sent < 0) && pal_file_error_is_fatal(errno)) { - break; - } - } - - if (recovered) { - continue; - } - } - - remaining = 1U; - break; } pal_socket_uncork(session->data_fd); @@ -539,9 +479,9 @@ ftp_error_t cmd_STOR(ftp_session_t *session, const char *args) { int fd = -1; err = upload_open_file(session, write_path, open_flags, "Cannot create file.", &fd); - if (err != FTP_OK) { + if (err != FTP_OK || fd < 0) { if (use_atomic != 0) (void)unlink(tmp_path); - return err; + return err != FTP_OK ? err : FTP_ERR_FILE_OPEN; } if (session->restart_offset > 0) { @@ -741,7 +681,8 @@ ftp_error_t cmd_APPE(ftp_session_t *session, const char *args) { int fd = -1; err = upload_open_file(session, resolved, open_flags, "Cannot open file.", &fd); - if (err != FTP_OK) return err; + if (err != FTP_OK || fd < 0) + return err != FTP_OK ? err : FTP_ERR_FILE_OPEN; if (session->restart_offset > 0 && lseek(fd, session->restart_offset, SEEK_SET) < 0) { diff --git a/src/ftp/ftp_instance.c b/src/ftp/ftp_instance.c index a422470..fe7af42 100644 --- a/src/ftp/ftp_instance.c +++ b/src/ftp/ftp_instance.c @@ -29,8 +29,13 @@ SOFTWARE. #include "ftp_instance.h" +#include +#include #include #include +#include +#include +#include #include #include @@ -77,3 +82,100 @@ uint64_t ftp_daemon_start_monotonic_ns(void) { (void)ftp_daemon_instance_id(); return atomic_load(&g_daemon_start_ns); } + +int ftp_instance_identity_format(char *out, size_t capacity, int pid, + uint64_t token) { + if (out == NULL || capacity == 0U || pid <= 1) { + return -1; + } + int written = snprintf(out, capacity, "%d %016llx\n", pid, + (unsigned long long)token); + if (written <= 0 || (size_t)written >= capacity) { + return -1; + } + return 0; +} + +int ftp_instance_identity_parse(const char *text, int *pid, uint64_t *token) { + if (text == NULL) { + return -1; + } + + errno = 0; + char *pid_end = NULL; + long parsed_pid = strtol(text, &pid_end, 10); + if (pid_end == text || errno != 0 || parsed_pid <= 1L || parsed_pid > INT_MAX) { + return -1; + } + if (*pid_end != ' ' && *pid_end != '\t') { + return -1; + } + + const char *token_start = pid_end + 1; + while (*token_start == ' ' || *token_start == '\t') { + token_start++; + } + + errno = 0; + char *token_end = NULL; + unsigned long long parsed_token = strtoull(token_start, &token_end, 16); + if (token_end == token_start || errno != 0) { + return -1; + } + while (*token_end == ' ' || *token_end == '\t') { + token_end++; + } + if (*token_end != '\0' && *token_end != '\n' && *token_end != '\r') { + return -1; + } + + if (pid != NULL) { + *pid = (int)parsed_pid; + } + if (token != NULL) { + *token = (uint64_t)parsed_token; + } + return 0; +} + +int ftp_instance_stop_command_format(char *out, size_t capacity, + uint64_t token) { + if (out == NULL || capacity == 0U) { + return -1; + } + int written = snprintf(out, capacity, "STOP %016llx\n", + (unsigned long long)token); + if (written <= 0 || (size_t)written >= capacity) { + return -1; + } + return 0; +} + +int ftp_instance_stop_command_parse(const char *line, uint64_t *token) { + static const char prefix[] = "STOP "; + if (line == NULL) { + return -1; + } + if (strncmp(line, prefix, sizeof(prefix) - 1U) != 0) { + return -1; + } + + const char *start = line + sizeof(prefix) - 1U; + errno = 0; + char *end = NULL; + unsigned long long parsed = strtoull(start, &end, 16); + if (end == start || errno != 0) { + return -1; + } + while (*end == ' ' || *end == '\t') { + end++; + } + if (*end != '\0' && *end != '\n' && *end != '\r') { + return -1; + } + + if (token != NULL) { + *token = (uint64_t)parsed; + } + return 0; +} diff --git a/src/platform/fileio/basic.c b/src/platform/fileio/basic.c index ce8e627..b23ff2e 100644 --- a/src/platform/fileio/basic.c +++ b/src/platform/fileio/basic.c @@ -30,14 +30,6 @@ SOFTWARE. #include #include -#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 @@ -179,12 +171,7 @@ ssize_t pal_file_write_all(int fd, const void *buffer, size_t count) { 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); + ssize_t n = write(fd, p + total, count - total); if (n > 0) { total += (size_t)n; continue; diff --git a/src/platform/fileio/copy.c b/src/platform/fileio/copy.c index 0e32980..f3862dd 100644 --- a/src/platform/fileio/copy.c +++ b/src/platform/fileio/copy.c @@ -518,8 +518,6 @@ pal_file_copy_atomic_ex(const char *src_path, const char *dst_path, } /* 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. */ diff --git a/src/platform/fileio/directory.c b/src/platform/fileio/directory.c index bff7bc9..1e240a6 100644 --- a/src/platform/fileio/directory.c +++ b/src/platform/fileio/directory.c @@ -100,6 +100,32 @@ ftp_error_t pal_dir_create(const char *path, mode_t mode) { /** * @brief Remove directory */ +ftp_error_t pal_dir_create_recursive(const char *path, mode_t mode) { + if (path == NULL || path[0] == '\0') return FTP_ERR_INVALID_PARAM; + size_t len = strlen(path); + if (len >= FTP_PATH_MAX) return FTP_ERR_PATH_TOO_LONG; + + char work[FTP_PATH_MAX]; + memcpy(work, path, len + 1U); + for (size_t i = 1U; i <= len; i++) { + if (work[i] != '/' && work[i] != '\0') continue; + char saved = work[i]; + work[i] = '\0'; + if (work[0] != '\0') { + ftp_error_t err = pal_dir_create(work, mode); + if (err == FTP_ERR_DIR_EXISTS) { + int is_dir = pal_path_is_directory(work); + if (is_dir != 1) return is_dir < 0 ? (ftp_error_t)is_dir + : FTP_ERR_INVALID_PARAM; + } else if (err != FTP_OK) { + return err; + } + } + work[i] = saved; + } + return FTP_OK; +} + ftp_error_t pal_dir_remove(const char *path) { if (path == NULL) { return FTP_ERR_INVALID_PARAM; diff --git a/src/platform/fileio/sendfile.c b/src/platform/fileio/sendfile.c index 361d7d3..b28717a 100644 --- a/src/platform/fileio/sendfile.c +++ b/src/platform/fileio/sendfile.c @@ -104,3 +104,20 @@ ssize_t pal_sendfile(int sock_fd, int file_fd, off_t *offset, size_t count) { return nsent; #endif } +ssize_t pal_sendfile_retry(int sock_fd, int file_fd, off_t *offset, size_t count, + unsigned retries, unsigned sleep_us) { + unsigned attempt = 0U; + for (;;) { + ssize_t sent = pal_sendfile(sock_fd, file_fd, offset, count); + if (sent > 0) return sent; + int error = errno; + if (error == EINTR) continue; + if (sent < 0 && pal_file_error_is_fatal(error)) return -1; + if (error != EAGAIN && error != EWOULDBLOCK) return sent; + if (attempt++ >= retries) { + errno = error; + return -1; + } + if (sleep_us > 0U) usleep(sleep_us); + } +} diff --git a/tests/test_fileio_basic.c b/tests/test_fileio_basic.c index af91a25..4ab2d45 100644 --- a/tests/test_fileio_basic.c +++ b/tests/test_fileio_basic.c @@ -105,6 +105,32 @@ static int test_sendfile_path(const char *path) { return 0; } +static int test_recursive_mkdir(void) { + char root[] = "/tmp/zftpd-mkdir-XXXXXX"; + CHECK(mkdtemp(root) != NULL); + + char nested[FTP_PATH_MAX]; + CHECK(snprintf(nested, sizeof(nested), "%s/a/b/c", root) > 0); + CHECK(pal_dir_create_recursive(nested, 0700) == FTP_OK); + CHECK(pal_path_is_directory(nested) == 1); + + char conflict[FTP_PATH_MAX], child[FTP_PATH_MAX]; + CHECK(snprintf(conflict, sizeof(conflict), "%s/file", root) > 0); + int fd = open(conflict, O_WRONLY | O_CREAT | O_TRUNC, 0600); + CHECK(fd >= 0); + CHECK(close(fd) == 0); + CHECK(snprintf(child, sizeof(child), "%s/child", conflict) > 0); + CHECK(pal_dir_create_recursive(child, 0700) == FTP_ERR_INVALID_PARAM); + + CHECK(unlink(conflict) == 0); + CHECK(rmdir(nested) == 0); + char b[FTP_PATH_MAX], a[FTP_PATH_MAX]; + CHECK(snprintf(b, sizeof(b), "%s/a/b", root) > 0); + CHECK(snprintf(a, sizeof(a), "%s/a", root) > 0); + CHECK(rmdir(b) == 0 && rmdir(a) == 0 && rmdir(root) == 0); + return 0; +} + int main(void) { char path[] = "/tmp/zftpd-fileio-basic-XXXXXX"; int seed_fd = mkstemp(path); @@ -113,6 +139,7 @@ int main(void) { CHECK(test_basic_io(path) == 0); CHECK(test_sendfile_path(path) == 0); + CHECK(test_recursive_mkdir() == 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); diff --git a/tests/test_instance.c b/tests/test_instance.c index f16117e..5ac708a 100644 --- a/tests/test_instance.c +++ b/tests/test_instance.c @@ -32,6 +32,7 @@ SOFTWARE. #include #include +#include int main(void) { ftp_instance_init(); @@ -64,6 +65,48 @@ int main(void) { if (pal_listen_fd_alive(-1) != 0) return 10; + char line[80]; + char command[64]; + int pid = 0; + uint64_t token = 0U; + + if (ftp_instance_identity_format(line, sizeof(line), 4242, 0x0123456789abcdefULL) != 0) + return 11; + if (strcmp(line, "4242 0123456789abcdef\n") != 0) return 12; + if (ftp_instance_identity_parse(line, &pid, &token) != 0 || pid != 4242 || + token != 0x0123456789abcdefULL) + return 13; + + /* A buffer that cannot hold the whole line is refused, never truncated. */ + if (ftp_instance_identity_format(line, 4U, 4242, 1U) == 0) return 14; + if (ftp_instance_identity_format(line, sizeof(line), 1, 1U) == 0) return 15; + + /* Malformed instance files must not look like a live payload. */ + if (ftp_instance_identity_parse("not-a-pid\n", &pid, &token) == 0) return 16; + if (ftp_instance_identity_parse("1 abc\n", &pid, &token) == 0) return 17; + if (ftp_instance_identity_parse("4242 \n", &pid, &token) == 0) return 18; + if (ftp_instance_identity_parse("4242 abc trailing\n", &pid, &token) == 0) return 19; + if (ftp_instance_identity_parse("4242 abc", &pid, &token) != 0 || + token != 0xabcULL) + return 20; + + if (ftp_instance_stop_command_format(command, sizeof(command), + 0xfeedfacecafebeefULL) != 0) + return 21; + if (strcmp(command, "STOP feedfacecafebeef\n") != 0) return 22; + if (ftp_instance_stop_command_parse(command, &token) != 0 || + token != 0xfeedfacecafebeefULL) + return 23; + + /* Only the exact command is accepted: a stray token or a different verb + * must never stop a running payload. */ + if (ftp_instance_stop_command_parse("STOP feedfacecafebeef extra\n", &token) == 0) + return 24; + if (ftp_instance_stop_command_parse("stop feedfacecafebeef\n", &token) == 0) + return 25; + if (ftp_instance_stop_command_parse("STOP\n", &token) == 0) return 26; + if (ftp_instance_stop_command_format(command, 6U, 1U) == 0) return 27; + printf("test_instance: ok (id=%016llx start=%llu)\n", (unsigned long long)id1, (unsigned long long)start1); return 0;