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;