From 8b2c1622e2e4cdd4d2025000ed879a2eefdf789e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 28 Sep 2025 23:00:46 +0200 Subject: [PATCH 01/10] io: add initial php_io copy structure and implementation --- main/io/php_io.c | 140 ++++++++++++++++++++++++++++ main/io/php_io_copy_bsd.c | 89 ++++++++++++++++++ main/io/php_io_copy_generic.c | 74 +++++++++++++++ main/io/php_io_copy_linux.c | 114 +++++++++++++++++++++++ main/io/php_io_copy_macos.c | 105 +++++++++++++++++++++ main/io/php_io_copy_solaris.c | 112 +++++++++++++++++++++++ main/io/php_io_copy_windows.c | 116 +++++++++++++++++++++++ main/io/php_io_internal.h | 28 ++++++ main/io/php_io_ring_generic.c | 31 +++++++ main/io/php_io_ring_linux.c | 168 ++++++++++++++++++++++++++++++++++ main/php_io.h | 144 +++++++++++++++++++++++++++++ 11 files changed, 1121 insertions(+) create mode 100644 main/io/php_io.c create mode 100644 main/io/php_io_copy_bsd.c create mode 100644 main/io/php_io_copy_generic.c create mode 100644 main/io/php_io_copy_linux.c create mode 100644 main/io/php_io_copy_macos.c create mode 100644 main/io/php_io_copy_solaris.c create mode 100644 main/io/php_io_copy_windows.c create mode 100644 main/io/php_io_internal.h create mode 100644 main/io/php_io_ring_generic.c create mode 100644 main/io/php_io_ring_linux.c create mode 100644 main/php_io.h diff --git a/main/io/php_io.c b/main/io/php_io.c new file mode 100644 index 0000000000000..aa3cb6ab84507 --- /dev/null +++ b/main/io/php_io.c @@ -0,0 +1,140 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_io.h" +#include "php_io_internal.h" +#include +#include + +#ifdef PHP_WIN32 +#include +#include +#else +#include +#endif + +/* Global instance pointer */ +static php_io *g_php_io = NULL; + +/* Global instance */ +static php_io g_php_io_instance; +static zend_bool g_php_io_initialized = 0; + +/* Factory function - selects best implementation for platform */ +PHPAPI php_io* php_io_create(void) +{ + if (g_php_io_initialized) { + return &g_php_io_instance; + } + + /* Initialize copy operations */ + php_io_register_copy(&g_php_io_instance.copy, &g_php_io_instance.capabilities); + + /* Initialize ring operations */ + php_io_register_ring(&g_php_io_instance.ring, &g_php_io_instance.capabilities); + + /* Set platform name */ +#ifdef __linux__ + g_php_io_instance.platform_name = "linux"; +#elif defined(_WIN32) + g_php_io_instance.platform_name = "windows"; +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__sun) + g_php_io_instance.platform_name = "bsd"; +#else + g_php_io_instance.platform_name = "generic"; +#endif + + g_php_io_initialized = 1; + return &g_php_io_instance; +} + +/* Get global instance (lazy initialization) */ +PHPAPI php_io* php_io_get(void) +{ + if (g_php_io == NULL) { + g_php_io = php_io_create(); + } + return g_php_io; +} + +/* Detect file descriptor type */ +PHPAPI php_io_fd_type php_io_detect_fd_type(int fd) +{ + struct stat st; + + if (fstat(fd, &st) != 0) { + return PHP_IO_FD_UNKNOWN; + } + + if (S_ISREG(st.st_mode)) { + return PHP_IO_FD_FILE; + } else if (S_ISSOCK(st.st_mode)) { + return PHP_IO_FD_SOCKET; + } else if (S_ISFIFO(st.st_mode)) { + return PHP_IO_FD_PIPE; + } + + /* Additional socket detection for systems where S_ISSOCK doesn't work */ + int sock_type; + socklen_t sock_type_len = sizeof(sock_type); + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &sock_type, &sock_type_len) == 0) { + return PHP_IO_FD_SOCKET; + } + + return PHP_IO_FD_UNKNOWN; +} + +/* Extract file descriptor and type from php_stream */ +PHPAPI zend_result php_io_get_fd_and_type(php_stream *stream, int *fd, php_io_fd_type *type) +{ + if (php_stream_cast(stream, PHP_STREAM_AS_FD, (void**)fd, 0) != SUCCESS) { + return FAILURE; + } + + *type = php_io_detect_fd_type(*fd); + return SUCCESS; +} + +/* Generic read/write fallback implementation */ +zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + char buf[8192]; + size_t total_copied = 0; + size_t remaining = len; + + while (remaining > 0 && total_copied < len) { + size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); + ssize_t bytes_read = read(src_fd, buf, to_read); + + if (bytes_read <= 0) { + break; /* EOF or error */ + } + + ssize_t bytes_written = write(dest_fd, buf, bytes_read); + if (bytes_written <= 0) { + break; /* Error */ + } + + total_copied += bytes_written; + remaining -= bytes_written; + + if (bytes_written != bytes_read) { + break; /* Partial write */ + } + } + + *copied = total_copied; + return total_copied > 0 ? SUCCESS : FAILURE; +} diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c new file mode 100644 index 0000000000000..8a5c42de7ee0b --- /dev/null +++ b/main/io/php_io_copy_bsd.c @@ -0,0 +1,89 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) + +#include "php_io_internal.h" +#include +#include + +#ifdef HAVE_SENDFILE +#include +#include +#endif + +static zend_result php_io_copy_bsd_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* BSD variants don't have a special file-to-file copy optimization */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_bsd_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_SENDFILE + /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ + /* This signature is shared by FreeBSD, OpenBSD, and NetBSD */ + off_t sbytes = 0; + int result = sendfile(src_fd, dest_fd, 0, len, NULL, &sbytes, 0); + + if (result == 0) { + /* Success - entire amount was sent */ + *copied = len; + return SUCCESS; + } else if (result == -1 && sbytes > 0) { + /* Partial send - some data was transferred */ + *copied = sbytes; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + case EBUSY: + /* Would block or busy - could retry, but fall back for now */ + break; + case EINVAL: + /* Invalid arguments - likely not a regular file or socket */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + default: + /* Other errors */ + break; + } + } +#endif /* HAVE_SENDFILE */ + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_bsd_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* No BSD-specific optimization for socket to fd */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +{ + ops->file_to_file = php_io_copy_bsd_file_to_file; + ops->file_to_socket = php_io_copy_bsd_file_to_socket; + ops->socket_to_fd = php_io_copy_bsd_socket_to_fd; + +#ifdef HAVE_SENDFILE + *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; +#endif +} + +#endif /* FreeBSD, OpenBSD, NetBSD */ diff --git a/main/io/php_io_copy_generic.c b/main/io/php_io_copy_generic.c new file mode 100644 index 0000000000000..1bea6f0984d62 --- /dev/null +++ b/main/io/php_io_copy_generic.c @@ -0,0 +1,74 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_io_internal.h" + +/* Generic implementations using standard read/write */ + +zend_result php_io_copy_generic_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +zend_result php_io_copy_generic_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +zend_result php_io_copy_generic_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +/* Generic copy operations vtable */ +static php_io_copy_ops php_io_copy_ops_generic = { + .file_to_file = php_io_copy_generic_file_to_file, + .file_to_socket = php_io_copy_generic_file_to_socket, + .socket_to_fd = php_io_copy_generic_socket_to_fd, +}; + +/* Generic ring operations vtable (no ring support) */ +static php_io_ring_ops php_io_ring_ops_generic = { + .create = NULL, + .submit = NULL, + .wait_cqe = NULL, + .cqe_seen = NULL, + .destroy = NULL, + .capabilities = 0, +}; + +/* Generic php_io instance */ +static php_io php_io_instance_generic = { + .copy = { + .file_to_file = php_io_copy_generic_file_to_file, + .file_to_socket = php_io_copy_generic_file_to_socket, + .socket_to_fd = php_io_copy_generic_socket_to_fd, + }, + .ring = { + .create = NULL, + .submit = NULL, + .wait_cqe = NULL, + .cqe_seen = NULL, + .destroy = NULL, + .capabilities = 0, + }, + .platform_name = "generic", + .capabilities = 0, +}; + +/* Registration function */ +php_io* php_io_register_generic(void) +{ + return &php_io_instance_generic; +} diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c new file mode 100644 index 0000000000000..e269a390d82c5 --- /dev/null +++ b/main/io/php_io_copy_linux.c @@ -0,0 +1,114 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifdef __linux__ + +#include "php_io_internal.h" +#include +#include + +/* Linux-specific includes */ +#ifdef HAVE_COPY_FILE_RANGE +#include +#endif + +#ifdef HAVE_SENDFILE +#include +#endif + +/* Forward declaration for ring registration function */ +php_io_ring_ops* php_io_ring_register_linux(void); + +/* copy_file_range wrapper for older systems */ +#ifdef HAVE_COPY_FILE_RANGE +static ssize_t copy_file_range_wrapper(int fd_in, off_t *off_in, int fd_out, off_t *off_out, + size_t len, unsigned int flags) +{ + return syscall(SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); +} +#endif + +zend_result php_io_copy_linux_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_COPY_FILE_RANGE + /* Try copy_file_range first - kernel-level optimization */ + ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, len, 0); + + if (result > 0) { + *copied = (size_t)result; + return SUCCESS; + } + + /* If copy_file_range fails, fall through to generic implementation */ + if (result == -1) { + switch (errno) { + case EINVAL: + case EXDEV: + case ENOSYS: + /* Expected failures - fall back to generic copy */ + break; + default: + /* Unexpected error */ + *copied = 0; + return FAILURE; + } + } +#endif + + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +zend_result php_io_copy_linux_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_SENDFILE + /* Use sendfile for zero-copy file to socket transfer */ + off_t offset = 0; + ssize_t result = sendfile(dest_fd, src_fd, &offset, len); + + if (result > 0) { + *copied = (size_t)result; + return SUCCESS; + } + + /* Handle partial sends and errors */ + if (result == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* Would block - for now, fall back to generic copy */ + /* TODO: Could implement epoll-based retry here */ + } + /* Other errors fall through to generic copy */ + } +#endif + + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +zend_result php_io_copy_linux_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* No Linux-specific optimization for socket to fd */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +{ + ops->file_to_file = php_io_copy_linux_file_to_file; + ops->file_to_socket = php_io_copy_linux_file_to_socket; + ops->socket_to_fd = php_io_copy_linux_socket_to_fd; + + *capabilities |= PHP_IO_CAP_ZERO_COPY | PHP_IO_CAP_SENDFILE; +} + +#endif /* __linux__ */ diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c new file mode 100644 index 0000000000000..4f1c22fe61525 --- /dev/null +++ b/main/io/php_io_copy_macos.c @@ -0,0 +1,105 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifdef __APPLE__ + +#include "php_io_internal.h" +#include +#include + +#ifdef HAVE_SENDFILE +#include +#include +#endif + +#ifdef HAVE_COPYFILE +#include +#endif + +static zend_result php_io_copy_macos_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_COPYFILE + /* macOS copyfile() can be used, but it's designed for whole files */ + /* For partial copies, it's complex to use properly */ + /* TODO: Could implement copyfile() for whole-file copies in the future */ +#endif + + /* For now, use generic implementation for file-to-file */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_macos_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_SENDFILE + /* macOS sendfile signature: sendfile(fd, s, offset, len, hdtr, flags) */ + /* Note: len is passed by reference and updated with bytes sent */ + off_t len_sent = len; + int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); + + if (result == 0) { + /* Success */ + *copied = len_sent; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + /* Would block - could be partial send */ + if (len_sent > 0) { + *copied = len_sent; + return SUCCESS; + } + break; + case EINVAL: + /* Invalid arguments - likely not a regular file or socket */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + case EPIPE: + /* Broken pipe */ + break; + default: + /* Other errors */ + break; + } + } +#endif /* HAVE_SENDFILE */ + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_macos_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* No macOS-specific optimization for socket to fd */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +{ + ops->file_to_file = php_io_copy_macos_file_to_file; + ops->file_to_socket = php_io_copy_macos_file_to_socket; + ops->socket_to_fd = php_io_copy_macos_socket_to_fd; + +#ifdef HAVE_SENDFILE + *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; +#endif + +#ifdef HAVE_COPYFILE + /* Could add a capability flag for copyfile support */ +#endif +} + +#endif /* __APPLE__ */ diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c new file mode 100644 index 0000000000000..86c9149a1f4a7 --- /dev/null +++ b/main/io/php_io_copy_solaris.c @@ -0,0 +1,112 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifdef __sun + +#include "php_io_internal.h" +#include +#include + +#ifdef HAVE_SENDFILEV +#include +#endif + +static zend_result php_io_copy_solaris_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_SENDFILEV + /* Solaris sendfilev can potentially do file-to-file transfers */ + /* But it's complex and mainly designed for file-to-socket */ + /* For now, use generic implementation */ +#endif + + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_solaris_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ +#ifdef HAVE_SENDFILEV + /* Solaris sendfilev - very powerful but complex API */ + struct sendfilevec sfv; + size_t xferred = 0; + + /* Set up the sendfile vector */ + sfv.sfv_fd = src_fd; /* Source file descriptor */ + sfv.sfv_flag = SFV_FD; /* sfv_fd is a file descriptor */ + sfv.sfv_off = 0; /* Offset in the file */ + sfv.sfv_len = len; /* Number of bytes to send */ + + /* Perform the sendfile operation */ + ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); + + if (result == 0) { + /* Success - all data transferred */ + *copied = xferred; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + /* Would block - partial transfer possible */ + if (xferred > 0) { + *copied = xferred; + return SUCCESS; + } + break; + case EINVAL: + /* Invalid arguments */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + case EPIPE: + /* Broken pipe */ + break; + case EAFNOSUPPORT: + /* Address family not supported */ + break; + default: + /* Other errors */ + break; + } + + /* Even on error, some data might have been transferred */ + if (xferred > 0) { + *copied = xferred; + return SUCCESS; + } + } +#endif /* HAVE_SENDFILEV */ + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_solaris_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* No Solaris-specific optimization for socket to fd */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +{ + ops->file_to_file = php_io_copy_solaris_file_to_file; + ops->file_to_socket = php_io_copy_solaris_file_to_socket; + ops->socket_to_fd = php_io_copy_solaris_socket_to_fd; + +#ifdef HAVE_SENDFILEV + *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; +#endif +} + +#endif /* __sun */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c new file mode 100644 index 0000000000000..a653f4208b13a --- /dev/null +++ b/main/io/php_io_copy_windows.c @@ -0,0 +1,116 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: PHP Development Team | + +----------------------------------------------------------------------+ +*/ + +#include "php_io_internal.h" + +#ifdef PHP_WIN32 + +#include +#include +#include + +static zend_result php_io_copy_windows_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* Try CopyFileEx for optimal file-to-file copying */ + HANDLE src_handle = (HANDLE)_get_osfhandle(src_fd); + HANDLE dest_handle = (HANDLE)_get_osfhandle(dest_fd); + + if (src_handle != INVALID_HANDLE_VALUE && dest_handle != INVALID_HANDLE_VALUE) { + /* Get source file size to determine copy length */ + LARGE_INTEGER file_size; + if (GetFileSizeEx(src_handle, &file_size)) { + DWORD bytes_to_copy = (DWORD)min(len, (size_t)file_size.QuadPart); + + /* Use ReadFile/WriteFile for partial copies since CopyFileEx copies entire files */ + char buffer[65536]; + DWORD total_copied = 0; + + while (total_copied < bytes_to_copy) { + DWORD to_read = min(sizeof(buffer), bytes_to_copy - total_copied); + DWORD bytes_read, bytes_written; + + if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { + break; + } + + if (bytes_read == 0) { + break; /* EOF */ + } + + if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { + break; + } + + total_copied += bytes_written; + + if (bytes_written != bytes_read) { + break; /* Partial write */ + } + } + + *copied = total_copied; + return total_copied > 0 ? SUCCESS : FAILURE; + } + } + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_windows_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* Use TransmitFile for zero-copy file to socket transfer */ + HANDLE file_handle = (HANDLE)_get_osfhandle(src_fd); + SOCKET sock = (SOCKET)dest_fd; + + if (file_handle != INVALID_HANDLE_VALUE && sock != INVALID_SOCKET) { + /* TransmitFile can send entire file or partial */ + DWORD bytes_to_send = (DWORD)len; + + if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { + *copied = bytes_to_send; + return SUCCESS; + } + + /* TransmitFile failed, check if it's a recoverable error */ + int error = WSAGetLastError(); + if (error == WSAENOTSOCK) { + /* dest_fd is not a socket, fall back to generic copy */ + } else { + /* Other TransmitFile error, could be network related */ + } + } + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +static zend_result php_io_copy_windows_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +{ + /* No Windows-specific optimization for socket to fd */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); +} + +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +{ + ops->file_to_file = php_io_copy_windows_file_to_file; + ops->file_to_socket = php_io_copy_windows_file_to_socket; + ops->socket_to_fd = php_io_copy_windows_socket_to_fd; + + *capabilities |= PHP_IO_CAP_ZERO_COPY | PHP_IO_CAP_SENDFILE; +} + +#endif /* PHP_WIN32 */ diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h new file mode 100644 index 0000000000000..74d9b7d08541b --- /dev/null +++ b/main/io/php_io_internal.h @@ -0,0 +1,28 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_INTERNAL_H +#define PHP_IO_INTERNAL_H + +#include "php_io.h" + +/* Internal utility functions */ +zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied); + +/* Single registration functions - implemented once per platform file */ +void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities); +void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities); + + +#endif /* PHP_IO_INTERNAL_H */ diff --git a/main/io/php_io_ring_generic.c b/main/io/php_io_ring_generic.c new file mode 100644 index 0000000000000..8479e84f47940 --- /dev/null +++ b/main/io/php_io_ring_generic.c @@ -0,0 +1,31 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_io_internal.h" + +/* Generic fallback - used when no platform-specific ring implementation is compiled */ +#if !defined(__linux__) && !defined(_WIN32) && !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) && !defined(__APPLE__) && !defined(__sun) + +void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities) +{ + /* No ring support in generic implementation */ + ops->create = NULL; + ops->submit = NULL; + ops->wait_cqe = NULL; + ops->cqe_seen = NULL; + ops->destroy = NULL; + ops->capabilities = 0; +} + +#endif diff --git a/main/io/php_io_ring_linux.c b/main/io/php_io_ring_linux.c new file mode 100644 index 0000000000000..d44b3a80693d2 --- /dev/null +++ b/main/io/php_io_ring_linux.c @@ -0,0 +1,168 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: PHP Development Team | + +----------------------------------------------------------------------+ +*/ + +#ifdef __linux__ + +#include "php_io_internal.h" + +#ifdef HAVE_IO_URING +#include +#include + +typedef struct { + struct io_uring ring; +} php_io_ring_linux; + +static php_io_ring* php_io_ring_linux_create(int queue_depth) +{ + php_io_ring_linux *lr = emalloc(sizeof(php_io_ring_linux)); + + if (io_uring_queue_init(queue_depth, &lr->ring, 0) == 0) { + return (php_io_ring*)lr; + } + + efree(lr); + return NULL; +} + +static zend_result php_io_ring_linux_submit(php_io_ring *ring, php_io_sqe *sqe) +{ + php_io_ring_linux *lr = (php_io_ring_linux*)ring; + struct io_uring_sqe *uring_sqe = io_uring_get_sqe(&lr->ring); + + if (!uring_sqe) { + return FAILURE; + } + + uring_sqe->user_data = sqe->user_data; + + switch (sqe->op) { + case PHP_IO_OP_SPLICE: + io_uring_prep_splice(uring_sqe, sqe->fd, sqe->params.splice.src_offset, + sqe->params.splice.dest_fd, sqe->params.splice.dest_offset, + sqe->params.splice.len, 0); + break; + case PHP_IO_OP_READ: + io_uring_prep_read(uring_sqe, sqe->fd, sqe->params.rw.buf, + sqe->params.rw.len, sqe->params.rw.offset); + break; + case PHP_IO_OP_WRITE: + io_uring_prep_write(uring_sqe, sqe->fd, sqe->params.rw.buf, + sqe->params.rw.len, sqe->params.rw.offset); + break; + default: + return FAILURE; + } + + return io_uring_submit(&lr->ring) > 0 ? SUCCESS : FAILURE; +} + +static int php_io_ring_linux_wait_cqe(php_io_ring *ring, php_io_cqe **cqe) +{ + php_io_ring_linux *lr = (php_io_ring_linux*)ring; + struct io_uring_cqe *uring_cqe; + static php_io_cqe php_cqe; + + int ret = io_uring_wait_cqe(&lr->ring, &uring_cqe); + if (ret == 0) { + php_cqe.user_data = uring_cqe->user_data; + php_cqe.result = uring_cqe->res; + php_cqe.flags = uring_cqe->flags; + *cqe = &php_cqe; + return 1; + } + + return 0; +} + +static void php_io_ring_linux_cqe_seen(php_io_ring *ring, php_io_cqe *cqe) +{ + php_io_ring_linux *lr = (php_io_ring_linux*)ring; + struct io_uring_cqe *uring_cqe; + + /* Find the corresponding io_uring cqe and mark it as seen */ + if (io_uring_peek_cqe(&lr->ring, &uring_cqe) == 0) { + if (uring_cqe->user_data == cqe->user_data) { + io_uring_cqe_seen(&lr->ring, uring_cqe); + } + } +} + +static void php_io_ring_linux_destroy(php_io_ring *ring) +{ + php_io_ring_linux *lr = (php_io_ring_linux*)ring; + io_uring_queue_exit(&lr->ring); + efree(lr); +} + +/* Check if io_uring is available and supports necessary features */ +static zend_bool php_io_ring_linux_detect_support(void) +{ + struct utsname uts; + if (uname(&uts) != 0) { + return 0; + } + + int major, minor, patch; + if (sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch) != 3) { + return 0; + } + + /* Require kernel 5.7+ for reliable splice support */ + int version = major * 1000000 + minor * 1000 + patch; + if (version < 5007000) { + return 0; + } + + /* Test if io_uring actually works */ + struct io_uring test_ring; + if (io_uring_queue_init(1, &test_ring, 0) == 0) { + io_uring_queue_exit(&test_ring); + return 1; + } + + return 0; +} + +#endif /* HAVE_IO_URING */ + +void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities) +{ +#ifdef HAVE_IO_URING + if (php_io_ring_linux_detect_support()) { + ops->create = php_io_ring_linux_create; + ops->submit = php_io_ring_linux_submit; + ops->wait_cqe = php_io_ring_linux_wait_cqe; + ops->cqe_seen = php_io_ring_linux_cqe_seen; + ops->destroy = php_io_ring_linux_destroy; + ops->capabilities = PHP_IO_RING_CAP_SPLICE | PHP_IO_RING_CAP_READ | + PHP_IO_RING_CAP_WRITE | PHP_IO_RING_CAP_BATCH; + + *capabilities |= PHP_IO_CAP_RING_SUPPORT; + return; + } +#endif + + /* No ring support available */ + ops->create = NULL; + ops->submit = NULL; + ops->wait_cqe = NULL; + ops->cqe_seen = NULL; + ops->destroy = NULL; + ops->capabilities = 0; +} + +#endif /* __linux__ */ diff --git a/main/php_io.h b/main/php_io.h new file mode 100644 index 0000000000000..17b25d24bea2c --- /dev/null +++ b/main/php_io.h @@ -0,0 +1,144 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_H +#define PHP_IO_H + +#include "php.h" + +/* Forward declarations */ +typedef struct php_io php_io; +typedef struct php_io_ring php_io_ring; + +/* Operation types for ring API */ +typedef enum { + PHP_IO_OP_READ, + PHP_IO_OP_WRITE, + PHP_IO_OP_SPLICE, +} php_io_op_type; + +/* File descriptor types */ +typedef enum { + PHP_IO_FD_FILE, + PHP_IO_FD_SOCKET, + PHP_IO_FD_PIPE, + PHP_IO_FD_UNKNOWN, +} php_io_fd_type; + +/* Ring-specific capabilities */ +#define PHP_IO_RING_CAP_SPLICE (1 << 0) +#define PHP_IO_RING_CAP_READ (1 << 1) +#define PHP_IO_RING_CAP_WRITE (1 << 2) +#define PHP_IO_RING_CAP_BATCH (1 << 3) + +/* General I/O capabilities */ +#define PHP_IO_CAP_RING_SUPPORT (1 << 0) +#define PHP_IO_CAP_ZERO_COPY (1 << 1) +#define PHP_IO_CAP_SENDFILE (1 << 2) + +/* Ring API structures */ +typedef struct php_io_sqe { + php_io_op_type op; + uint64_t user_data; + int fd; + union { + struct { + int dest_fd; + size_t len; + off_t src_offset; + off_t dest_offset; + } splice; + struct { + void *buf; + size_t len; + off_t offset; + } rw; + } params; +} php_io_sqe; + +typedef struct php_io_cqe { + uint64_t user_data; + ssize_t result; + uint32_t flags; +} php_io_cqe; + +/* Ring operations vtable */ +typedef struct php_io_ring_ops { + php_io_ring* (*create)(int queue_depth); + zend_result (*submit)(php_io_ring *ring, php_io_sqe *sqe); + int (*wait_cqe)(php_io_ring *ring, php_io_cqe **cqe); + void (*cqe_seen)(php_io_ring *ring, php_io_cqe *cqe); + void (*destroy)(php_io_ring *ring); + uint32_t capabilities; +} php_io_ring_ops; + +/* Synchronous copy operations vtable */ +typedef struct php_io_copy_ops { + zend_result (*file_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*file_to_socket)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*socket_to_fd)(int src_fd, int dest_fd, size_t len, size_t *copied); +} php_io_copy_ops; + +/* Main php_io structure */ +typedef struct php_io { + php_io_copy_ops copy; + php_io_ring_ops ring; + const char *platform_name; + uint32_t capabilities; +} php_io; + +/* Factory functions */ +PHPAPI php_io* php_io_create(void); +PHPAPI php_io* php_io_get(void); + +/* Utility functions */ +PHPAPI php_io_fd_type php_io_detect_fd_type(int fd); +PHPAPI zend_result php_io_get_fd_and_type(php_stream *stream, int *fd, php_io_fd_type *type); + +/* Ring API helper functions */ +static inline void php_io_ring_prep_splice(php_io_sqe *sqe, uint64_t user_data, + int src_fd, int dest_fd, size_t len) +{ + sqe->op = PHP_IO_OP_SPLICE; + sqe->user_data = user_data; + sqe->fd = src_fd; + sqe->params.splice.dest_fd = dest_fd; + sqe->params.splice.len = len; + sqe->params.splice.src_offset = 0; + sqe->params.splice.dest_offset = 0; +} + +static inline void php_io_ring_prep_read(php_io_sqe *sqe, uint64_t user_data, + int fd, void *buf, size_t len, off_t offset) +{ + sqe->op = PHP_IO_OP_READ; + sqe->user_data = user_data; + sqe->fd = fd; + sqe->params.rw.buf = buf; + sqe->params.rw.len = len; + sqe->params.rw.offset = offset; +} + +static inline void php_io_ring_prep_write(php_io_sqe *sqe, uint64_t user_data, + int fd, void *buf, size_t len, off_t offset) +{ + sqe->op = PHP_IO_OP_WRITE; + sqe->user_data = user_data; + sqe->fd = fd; + sqe->params.rw.buf = buf; + sqe->params.rw.len = len; + sqe->params.rw.offset = offset; +} + +#endif /* PHP_IO_H */ From cdb626fe4a80206cb8db9ec1f83e2871e8f45ba5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Nov 2025 20:29:40 +0100 Subject: [PATCH 02/10] io: refactore and simplify the io API --- main/io/php_io.c | 155 ++++++--------- .../{php_io_ring_generic.c => php_io_bsd.h} | 28 +-- main/io/php_io_copy_bsd.c | 91 ++++----- main/io/php_io_copy_generic.c | 74 ------- main/io/php_io_copy_linux.c | 182 ++++++++++++------ main/io/php_io_copy_macos.c | 107 +++++----- main/io/php_io_copy_solaris.c | 132 +++++-------- main/io/php_io_copy_windows.c | 167 ++++++++-------- main/io/php_io_generic.h | 28 +++ main/io/php_io_internal.h | 18 +- main/io/php_io_linux.h | 33 ++++ main/io/php_io_macos.h | 32 +++ main/io/php_io_ring_linux.c | 168 ---------------- main/io/php_io_solaris.h | 31 +++ main/io/php_io_windows.h | 32 +++ main/php_io.h | 96 +-------- 16 files changed, 564 insertions(+), 810 deletions(-) rename main/io/{php_io_ring_generic.c => php_io_bsd.h} (60%) delete mode 100644 main/io/php_io_copy_generic.c create mode 100644 main/io/php_io_generic.h create mode 100644 main/io/php_io_linux.h create mode 100644 main/io/php_io_macos.h delete mode 100644 main/io/php_io_ring_linux.c create mode 100644 main/io/php_io_solaris.h create mode 100644 main/io/php_io_windows.h diff --git a/main/io/php_io.c b/main/io/php_io.c index aa3cb6ab84507..c674272302d08 100644 --- a/main/io/php_io.c +++ b/main/io/php_io.c @@ -25,116 +25,73 @@ #include #endif -/* Global instance pointer */ -static php_io *g_php_io = NULL; +/* Global instance - initialized at compile time */ +static php_io php_io_instance = { + .copy = PHP_IO_PLATFORM_COPY_OPS, + .platform_name = PHP_IO_PLATFORM_NAME, +}; -/* Global instance */ -static php_io g_php_io_instance; -static zend_bool g_php_io_initialized = 0; - -/* Factory function - selects best implementation for platform */ -PHPAPI php_io* php_io_create(void) +/* Get global instance */ +PHPAPI php_io *php_io_get(void) { - if (g_php_io_initialized) { - return &g_php_io_instance; - } - - /* Initialize copy operations */ - php_io_register_copy(&g_php_io_instance.copy, &g_php_io_instance.capabilities); - - /* Initialize ring operations */ - php_io_register_ring(&g_php_io_instance.ring, &g_php_io_instance.capabilities); - - /* Set platform name */ -#ifdef __linux__ - g_php_io_instance.platform_name = "linux"; -#elif defined(_WIN32) - g_php_io_instance.platform_name = "windows"; -#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) || defined(__sun) - g_php_io_instance.platform_name = "bsd"; -#else - g_php_io_instance.platform_name = "generic"; -#endif - - g_php_io_initialized = 1; - return &g_php_io_instance; -} - -/* Get global instance (lazy initialization) */ -PHPAPI php_io* php_io_get(void) -{ - if (g_php_io == NULL) { - g_php_io = php_io_create(); - } - return g_php_io; + return &php_io_instance; } /* Detect file descriptor type */ PHPAPI php_io_fd_type php_io_detect_fd_type(int fd) { - struct stat st; - - if (fstat(fd, &st) != 0) { - return PHP_IO_FD_UNKNOWN; - } - - if (S_ISREG(st.st_mode)) { - return PHP_IO_FD_FILE; - } else if (S_ISSOCK(st.st_mode)) { - return PHP_IO_FD_SOCKET; - } else if (S_ISFIFO(st.st_mode)) { - return PHP_IO_FD_PIPE; - } - - /* Additional socket detection for systems where S_ISSOCK doesn't work */ - int sock_type; - socklen_t sock_type_len = sizeof(sock_type); - if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &sock_type, &sock_type_len) == 0) { - return PHP_IO_FD_SOCKET; - } - - return PHP_IO_FD_UNKNOWN; -} + struct stat st; -/* Extract file descriptor and type from php_stream */ -PHPAPI zend_result php_io_get_fd_and_type(php_stream *stream, int *fd, php_io_fd_type *type) -{ - if (php_stream_cast(stream, PHP_STREAM_AS_FD, (void**)fd, 0) != SUCCESS) { - return FAILURE; - } - - *type = php_io_detect_fd_type(*fd); - return SUCCESS; + if (fstat(fd, &st) != 0) { + return PHP_IO_FD_UNKNOWN; + } + + if (S_ISREG(st.st_mode)) { + return PHP_IO_FD_FILE; + } else if (S_ISSOCK(st.st_mode)) { + return PHP_IO_FD_SOCKET; + } else if (S_ISFIFO(st.st_mode)) { + return PHP_IO_FD_PIPE; + } + + /* Additional socket detection for systems where S_ISSOCK doesn't work */ + int sock_type; + socklen_t sock_type_len = sizeof(sock_type); + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &sock_type, &sock_type_len) == 0) { + return PHP_IO_FD_SOCKET; + } + + return PHP_IO_FD_UNKNOWN; } /* Generic read/write fallback implementation */ zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied) { - char buf[8192]; - size_t total_copied = 0; - size_t remaining = len; - - while (remaining > 0 && total_copied < len) { - size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); - ssize_t bytes_read = read(src_fd, buf, to_read); - - if (bytes_read <= 0) { - break; /* EOF or error */ - } - - ssize_t bytes_written = write(dest_fd, buf, bytes_read); - if (bytes_written <= 0) { - break; /* Error */ - } - - total_copied += bytes_written; - remaining -= bytes_written; - - if (bytes_written != bytes_read) { - break; /* Partial write */ - } - } - - *copied = total_copied; - return total_copied > 0 ? SUCCESS : FAILURE; + char buf[8192]; + size_t total_copied = 0; + size_t remaining = len; + + while (remaining > 0 && total_copied < len) { + size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); + ssize_t bytes_read = read(src_fd, buf, to_read); + + if (bytes_read <= 0) { + break; /* EOF or error */ + } + + ssize_t bytes_written = write(dest_fd, buf, bytes_read); + if (bytes_written <= 0) { + break; /* Error */ + } + + total_copied += bytes_written; + remaining -= bytes_written; + + if (bytes_written != bytes_read) { + break; /* Partial write */ + } + } + + *copied = total_copied; + return total_copied > 0 ? SUCCESS : FAILURE; } diff --git a/main/io/php_io_ring_generic.c b/main/io/php_io_bsd.h similarity index 60% rename from main/io/php_io_ring_generic.c rename to main/io/php_io_bsd.h index 8479e84f47940..2f62fc61dcd18 100644 --- a/main/io/php_io_ring_generic.c +++ b/main/io/php_io_bsd.h @@ -12,20 +12,20 @@ +----------------------------------------------------------------------+ */ -#include "php_io_internal.h" +#ifndef PHP_IO_BSD_H +#define PHP_IO_BSD_H -/* Generic fallback - used when no platform-specific ring implementation is compiled */ -#if !defined(__linux__) && !defined(_WIN32) && !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) && !defined(__APPLE__) && !defined(__sun) +/* Copy operations */ +zend_result php_io_bsd_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); -void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities) -{ - /* No ring support in generic implementation */ - ops->create = NULL; - ops->submit = NULL; - ops->wait_cqe = NULL; - ops->cqe_seen = NULL; - ops->destroy = NULL; - ops->capabilities = 0; -} +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_generic_copy_fallback, \ + .file_to_socket = php_io_bsd_copy_file_to_socket, \ + .socket_to_fd = php_io_generic_copy_fallback, \ + } -#endif +#define PHP_IO_PLATFORM_NAME "bsd" + +#endif /* PHP_IO_BSD_H */ diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c index 8a5c42de7ee0b..196a570a4290a 100644 --- a/main/io/php_io_copy_bsd.c +++ b/main/io/php_io_copy_bsd.c @@ -23,67 +23,44 @@ #include #endif -static zend_result php_io_copy_bsd_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* BSD variants don't have a special file-to-file copy optimization */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -static zend_result php_io_copy_bsd_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_bsd_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILE - /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ - /* This signature is shared by FreeBSD, OpenBSD, and NetBSD */ - off_t sbytes = 0; - int result = sendfile(src_fd, dest_fd, 0, len, NULL, &sbytes, 0); - - if (result == 0) { - /* Success - entire amount was sent */ - *copied = len; - return SUCCESS; - } else if (result == -1 && sbytes > 0) { - /* Partial send - some data was transferred */ - *copied = sbytes; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - case EBUSY: - /* Would block or busy - could retry, but fall back for now */ - break; - case EINVAL: - /* Invalid arguments - likely not a regular file or socket */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - default: - /* Other errors */ - break; - } - } -#endif /* HAVE_SENDFILE */ - - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ + /* This signature is shared by FreeBSD, OpenBSD, and NetBSD */ + off_t sbytes = 0; + int result = sendfile(src_fd, dest_fd, 0, len, NULL, &sbytes, 0); -static zend_result php_io_copy_bsd_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* No BSD-specific optimization for socket to fd */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + if (result == 0) { + /* Success - entire amount was sent */ + *copied = len; + return SUCCESS; + } else if (result == -1 && sbytes > 0) { + /* Partial send - some data was transferred */ + *copied = sbytes; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + case EBUSY: + /* Would block or busy - could retry, but fall back for now */ + break; + case EINVAL: + /* Invalid arguments - likely not a regular file or socket */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + default: + /* Other errors */ + break; + } + } +#endif /* HAVE_SENDFILE */ -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) -{ - ops->file_to_file = php_io_copy_bsd_file_to_file; - ops->file_to_socket = php_io_copy_bsd_file_to_socket; - ops->socket_to_fd = php_io_copy_bsd_socket_to_fd; - -#ifdef HAVE_SENDFILE - *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; -#endif + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } #endif /* FreeBSD, OpenBSD, NetBSD */ diff --git a/main/io/php_io_copy_generic.c b/main/io/php_io_copy_generic.c deleted file mode 100644 index 1bea6f0984d62..0000000000000 --- a/main/io/php_io_copy_generic.c +++ /dev/null @@ -1,74 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Jakub Zelenka | - +----------------------------------------------------------------------+ -*/ - -#include "php_io_internal.h" - -/* Generic implementations using standard read/write */ - -zend_result php_io_copy_generic_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -zend_result php_io_copy_generic_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -zend_result php_io_copy_generic_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -/* Generic copy operations vtable */ -static php_io_copy_ops php_io_copy_ops_generic = { - .file_to_file = php_io_copy_generic_file_to_file, - .file_to_socket = php_io_copy_generic_file_to_socket, - .socket_to_fd = php_io_copy_generic_socket_to_fd, -}; - -/* Generic ring operations vtable (no ring support) */ -static php_io_ring_ops php_io_ring_ops_generic = { - .create = NULL, - .submit = NULL, - .wait_cqe = NULL, - .cqe_seen = NULL, - .destroy = NULL, - .capabilities = 0, -}; - -/* Generic php_io instance */ -static php_io php_io_instance_generic = { - .copy = { - .file_to_file = php_io_copy_generic_file_to_file, - .file_to_socket = php_io_copy_generic_file_to_socket, - .socket_to_fd = php_io_copy_generic_socket_to_fd, - }, - .ring = { - .create = NULL, - .submit = NULL, - .wait_cqe = NULL, - .cqe_seen = NULL, - .destroy = NULL, - .capabilities = 0, - }, - .platform_name = "generic", - .capabilities = 0, -}; - -/* Registration function */ -php_io* php_io_register_generic(void) -{ - return &php_io_instance_generic; -} diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index e269a390d82c5..d9c66ea02f8d3 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -27,88 +27,144 @@ #include #endif -/* Forward declaration for ring registration function */ -php_io_ring_ops* php_io_ring_register_linux(void); +#ifdef HAVE_SPLICE +#include +#endif /* copy_file_range wrapper for older systems */ #ifdef HAVE_COPY_FILE_RANGE -static ssize_t copy_file_range_wrapper(int fd_in, off_t *off_in, int fd_out, off_t *off_out, - size_t len, unsigned int flags) +static ssize_t copy_file_range_wrapper( + int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) { - return syscall(SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); + return syscall(SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); } #endif -zend_result php_io_copy_linux_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_COPY_FILE_RANGE - /* Try copy_file_range first - kernel-level optimization */ - ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, len, 0); - - if (result > 0) { - *copied = (size_t)result; - return SUCCESS; - } - - /* If copy_file_range fails, fall through to generic implementation */ - if (result == -1) { - switch (errno) { - case EINVAL: - case EXDEV: - case ENOSYS: - /* Expected failures - fall back to generic copy */ - break; - default: - /* Unexpected error */ - *copied = 0; - return FAILURE; - } - } + /* Try copy_file_range first - kernel-level optimization */ + ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, len, 0); + + if (result > 0) { + *copied = (size_t) result; + return SUCCESS; + } + + /* If copy_file_range fails, fall through to generic implementation */ + if (result == -1) { + switch (errno) { + case EINVAL: + case EXDEV: + case ENOSYS: + /* Expected failures - fall back to generic copy */ + break; + default: + /* Unexpected error */ + *copied = 0; + return FAILURE; + } + } #endif - - /* Fallback to generic read/write loop */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -zend_result php_io_copy_linux_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_linux_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILE - /* Use sendfile for zero-copy file to socket transfer */ - off_t offset = 0; - ssize_t result = sendfile(dest_fd, src_fd, &offset, len); - - if (result > 0) { - *copied = (size_t)result; - return SUCCESS; - } - - /* Handle partial sends and errors */ - if (result == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - /* Would block - for now, fall back to generic copy */ - /* TODO: Could implement epoll-based retry here */ - } - /* Other errors fall through to generic copy */ - } + /* Use sendfile for zero-copy file to socket transfer */ + off_t offset = 0; + ssize_t result = sendfile(dest_fd, src_fd, &offset, len); + + if (result > 0) { + *copied = (size_t) result; + return SUCCESS; + } + + /* Handle partial sends and errors */ + if (result == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + /* Would block - for now, fall back to generic copy */ + /* TODO: Could implement epoll-based retry here */ + } + /* Other errors fall through to generic copy */ + } #endif - - /* Fallback to generic read/write loop */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} -zend_result php_io_copy_linux_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* No Linux-specific optimization for socket to fd */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +zend_result php_io_linux_copy_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) { - ops->file_to_file = php_io_copy_linux_file_to_file; - ops->file_to_socket = php_io_copy_linux_file_to_socket; - ops->socket_to_fd = php_io_copy_linux_socket_to_fd; - - *capabilities |= PHP_IO_CAP_ZERO_COPY | PHP_IO_CAP_SENDFILE; +#ifdef HAVE_SPLICE + /* Use splice for zero-copy socket to fd transfer */ + /* splice() can work directly between socket and file/pipe */ + size_t total_copied = 0; + size_t remaining = len; + + while (remaining > 0) { + /* splice from socket to destination fd */ + ssize_t result + = splice(src_fd, NULL, dest_fd, NULL, remaining, SPLICE_F_MOVE | SPLICE_F_MORE); + + if (result > 0) { + total_copied += result; + remaining -= result; + } else if (result == 0) { + /* EOF */ + break; + } else { + /* Error occurred */ + switch (errno) { + case EAGAIN: + case EWOULDBLOCK: + /* Would block - return what we've copied so far */ + if (total_copied > 0) { + *copied = total_copied; + return SUCCESS; + } + /* Fall through to generic if nothing copied yet */ + break; + case EINVAL: + /* splice not supported for these fds */ + if (total_copied > 0) { + /* We already copied some data, return success */ + *copied = total_copied; + return SUCCESS; + } + /* Fall through to generic */ + break; + case EPIPE: + /* Broken pipe */ + if (total_copied > 0) { + *copied = total_copied; + return SUCCESS; + } + *copied = 0; + return FAILURE; + default: + /* Other errors */ + if (total_copied > 0) { + *copied = total_copied; + return SUCCESS; + } + break; + } + break; + } + } + + if (total_copied > 0) { + *copied = total_copied; + return SUCCESS; + } +#endif /* HAVE_SPLICE */ + + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } #endif /* __linux__ */ diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c index 4f1c22fe61525..6064b13392375 100644 --- a/main/io/php_io_copy_macos.c +++ b/main/io/php_io_copy_macos.c @@ -27,79 +27,58 @@ #include #endif -static zend_result php_io_copy_macos_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_macos_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_COPYFILE - /* macOS copyfile() can be used, but it's designed for whole files */ - /* For partial copies, it's complex to use properly */ - /* TODO: Could implement copyfile() for whole-file copies in the future */ + /* macOS copyfile() can be used, but it's designed for whole files */ + /* For partial copies, it's complex to use properly */ + /* TODO: Could implement copyfile() for whole-file copies in the future */ #endif - - /* For now, use generic implementation for file-to-file */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -static zend_result php_io_copy_macos_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) -{ -#ifdef HAVE_SENDFILE - /* macOS sendfile signature: sendfile(fd, s, offset, len, hdtr, flags) */ - /* Note: len is passed by reference and updated with bytes sent */ - off_t len_sent = len; - int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); - - if (result == 0) { - /* Success */ - *copied = len_sent; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - /* Would block - could be partial send */ - if (len_sent > 0) { - *copied = len_sent; - return SUCCESS; - } - break; - case EINVAL: - /* Invalid arguments - likely not a regular file or socket */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - case EPIPE: - /* Broken pipe */ - break; - default: - /* Other errors */ - break; - } - } -#endif /* HAVE_SENDFILE */ - - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} -static zend_result php_io_copy_macos_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* No macOS-specific optimization for socket to fd */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + /* For now, use generic implementation for file-to-file */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +zend_result php_io_macos_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) { - ops->file_to_file = php_io_copy_macos_file_to_file; - ops->file_to_socket = php_io_copy_macos_file_to_socket; - ops->socket_to_fd = php_io_copy_macos_socket_to_fd; - #ifdef HAVE_SENDFILE - *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; -#endif + /* macOS sendfile signature: sendfile(fd, s, offset, len, hdtr, flags) */ + /* Note: len is passed by reference and updated with bytes sent */ + off_t len_sent = len; + int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); -#ifdef HAVE_COPYFILE - /* Could add a capability flag for copyfile support */ -#endif + if (result == 0) { + /* Success */ + *copied = len_sent; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + /* Would block - could be partial send */ + if (len_sent > 0) { + *copied = len_sent; + return SUCCESS; + } + break; + case EINVAL: + /* Invalid arguments - likely not a regular file or socket */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + case EPIPE: + /* Broken pipe */ + break; + default: + /* Other errors */ + break; + } + } +#endif /* HAVE_SENDFILE */ + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } #endif /* __APPLE__ */ diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c index 86c9149a1f4a7..6e3bebc215864 100644 --- a/main/io/php_io_copy_solaris.c +++ b/main/io/php_io_copy_solaris.c @@ -22,91 +22,63 @@ #include #endif -static zend_result php_io_copy_solaris_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_solaris_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILEV - /* Solaris sendfilev can potentially do file-to-file transfers */ - /* But it's complex and mainly designed for file-to-socket */ - /* For now, use generic implementation */ -#endif - - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + /* Solaris sendfilev - very powerful but complex API */ + struct sendfilevec sfv; + size_t xferred = 0; -static zend_result php_io_copy_solaris_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) -{ -#ifdef HAVE_SENDFILEV - /* Solaris sendfilev - very powerful but complex API */ - struct sendfilevec sfv; - size_t xferred = 0; - - /* Set up the sendfile vector */ - sfv.sfv_fd = src_fd; /* Source file descriptor */ - sfv.sfv_flag = SFV_FD; /* sfv_fd is a file descriptor */ - sfv.sfv_off = 0; /* Offset in the file */ - sfv.sfv_len = len; /* Number of bytes to send */ - - /* Perform the sendfile operation */ - ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); - - if (result == 0) { - /* Success - all data transferred */ - *copied = xferred; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - /* Would block - partial transfer possible */ - if (xferred > 0) { - *copied = xferred; - return SUCCESS; - } - break; - case EINVAL: - /* Invalid arguments */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - case EPIPE: - /* Broken pipe */ - break; - case EAFNOSUPPORT: - /* Address family not supported */ - break; - default: - /* Other errors */ - break; - } - - /* Even on error, some data might have been transferred */ - if (xferred > 0) { - *copied = xferred; - return SUCCESS; - } - } -#endif /* HAVE_SENDFILEV */ - - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + /* Set up the sendfile vector */ + sfv.sfv_fd = src_fd; /* Source file descriptor */ + sfv.sfv_flag = SFV_FD; /* sfv_fd is a file descriptor */ + sfv.sfv_off = 0; /* Offset in the file */ + sfv.sfv_len = len; /* Number of bytes to send */ -static zend_result php_io_copy_solaris_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* No Solaris-specific optimization for socket to fd */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + /* Perform the sendfile operation */ + ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) -{ - ops->file_to_file = php_io_copy_solaris_file_to_file; - ops->file_to_socket = php_io_copy_solaris_file_to_socket; - ops->socket_to_fd = php_io_copy_solaris_socket_to_fd; - -#ifdef HAVE_SENDFILEV - *capabilities |= PHP_IO_CAP_SENDFILE | PHP_IO_CAP_ZERO_COPY; -#endif + if (result == 0) { + /* Success - all data transferred */ + *copied = xferred; + return SUCCESS; + } else if (result == -1) { + /* Error occurred */ + switch (errno) { + case EAGAIN: + /* Would block - partial transfer possible */ + if (xferred > 0) { + *copied = xferred; + return SUCCESS; + } + break; + case EINVAL: + /* Invalid arguments */ + break; + case ENOTCONN: + /* Socket not connected */ + break; + case EPIPE: + /* Broken pipe */ + break; + case EAFNOSUPPORT: + /* Address family not supported */ + break; + default: + /* Other errors */ + break; + } + + /* Even on error, some data might have been transferred */ + if (xferred > 0) { + *copied = xferred; + return SUCCESS; + } + } +#endif /* HAVE_SENDFILEV */ + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } #endif /* __sun */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c index a653f4208b13a..146d7ed35adb9 100644 --- a/main/io/php_io_copy_windows.c +++ b/main/io/php_io_copy_windows.c @@ -1,16 +1,14 @@ /* +----------------------------------------------------------------------+ - | Copyright (c) The PHP Group | + | Copyright © The PHP Group and Contributors. | +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | - | If you did not receive a copy of the PHP license and are unable to | - | obtain it through the world-wide-web, please send a note to | - | license@php.net so we can mail you a copy immediately. | + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | +----------------------------------------------------------------------+ - | Authors: PHP Development Team | + | Authors: Jakub Zelenka | +----------------------------------------------------------------------+ */ @@ -22,95 +20,80 @@ #include #include -static zend_result php_io_copy_windows_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) { - /* Try CopyFileEx for optimal file-to-file copying */ - HANDLE src_handle = (HANDLE)_get_osfhandle(src_fd); - HANDLE dest_handle = (HANDLE)_get_osfhandle(dest_fd); - - if (src_handle != INVALID_HANDLE_VALUE && dest_handle != INVALID_HANDLE_VALUE) { - /* Get source file size to determine copy length */ - LARGE_INTEGER file_size; - if (GetFileSizeEx(src_handle, &file_size)) { - DWORD bytes_to_copy = (DWORD)min(len, (size_t)file_size.QuadPart); - - /* Use ReadFile/WriteFile for partial copies since CopyFileEx copies entire files */ - char buffer[65536]; - DWORD total_copied = 0; - - while (total_copied < bytes_to_copy) { - DWORD to_read = min(sizeof(buffer), bytes_to_copy - total_copied); - DWORD bytes_read, bytes_written; - - if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { - break; - } - - if (bytes_read == 0) { - break; /* EOF */ - } - - if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { - break; - } - - total_copied += bytes_written; - - if (bytes_written != bytes_read) { - break; /* Partial write */ - } - } - - *copied = total_copied; - return total_copied > 0 ? SUCCESS : FAILURE; - } - } - - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + /* Try CopyFileEx for optimal file-to-file copying */ + HANDLE src_handle = (HANDLE) _get_osfhandle(src_fd); + HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); -static zend_result php_io_copy_windows_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* Use TransmitFile for zero-copy file to socket transfer */ - HANDLE file_handle = (HANDLE)_get_osfhandle(src_fd); - SOCKET sock = (SOCKET)dest_fd; - - if (file_handle != INVALID_HANDLE_VALUE && sock != INVALID_SOCKET) { - /* TransmitFile can send entire file or partial */ - DWORD bytes_to_send = (DWORD)len; - - if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { - *copied = bytes_to_send; - return SUCCESS; - } - - /* TransmitFile failed, check if it's a recoverable error */ - int error = WSAGetLastError(); - if (error == WSAENOTSOCK) { - /* dest_fd is not a socket, fall back to generic copy */ - } else { - /* Other TransmitFile error, could be network related */ - } - } - - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} + if (src_handle != INVALID_HANDLE_VALUE && dest_handle != INVALID_HANDLE_VALUE) { + /* Get source file size to determine copy length */ + LARGE_INTEGER file_size; + if (GetFileSizeEx(src_handle, &file_size)) { + DWORD bytes_to_copy = (DWORD) min(len, (size_t) file_size.QuadPart); -static zend_result php_io_copy_windows_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) -{ - /* No Windows-specific optimization for socket to fd */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + /* Use ReadFile/WriteFile for partial copies since CopyFileEx copies entire files */ + char buffer[65536]; + DWORD total_copied = 0; + + while (total_copied < bytes_to_copy) { + DWORD to_read = min(sizeof(buffer), bytes_to_copy - total_copied); + DWORD bytes_read, bytes_written; + + if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { + break; + } + + if (bytes_read == 0) { + break; /* EOF */ + } + + if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { + break; + } + + total_copied += bytes_written; + + if (bytes_written != bytes_read) { + break; /* Partial write */ + } + } + + *copied = total_copied; + return total_copied > 0 ? SUCCESS : FAILURE; + } + } + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities) +zend_result php_io_windows_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) { - ops->file_to_file = php_io_copy_windows_file_to_file; - ops->file_to_socket = php_io_copy_windows_file_to_socket; - ops->socket_to_fd = php_io_copy_windows_socket_to_fd; - - *capabilities |= PHP_IO_CAP_ZERO_COPY | PHP_IO_CAP_SENDFILE; + /* Use TransmitFile for zero-copy file to socket transfer */ + HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); + SOCKET sock = (SOCKET) dest_fd; + + if (file_handle != INVALID_HANDLE_VALUE && sock != INVALID_SOCKET) { + /* TransmitFile can send entire file or partial */ + DWORD bytes_to_send = (DWORD) len; + + if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { + *copied = bytes_to_send; + return SUCCESS; + } + + /* TransmitFile failed, check if it's a recoverable error */ + int error = WSAGetLastError(); + if (error == WSAENOTSOCK) { + /* dest_fd is not a socket, fall back to generic copy */ + } else { + /* Other TransmitFile error, could be network related */ + } + } + + /* Fallback to generic implementation */ + return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } #endif /* PHP_WIN32 */ diff --git a/main/io/php_io_generic.h b/main/io/php_io_generic.h new file mode 100644 index 0000000000000..cc6968f0913c7 --- /dev/null +++ b/main/io/php_io_generic.h @@ -0,0 +1,28 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_GENERIC_H +#define PHP_IO_GENERIC_H + +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_generic_copy_fallback, \ + .file_to_socket = php_io_generic_copy_fallback, \ + .socket_to_fd = php_io_generic_copy_fallback, \ + } + +#define PHP_IO_PLATFORM_NAME "generic" + +#endif /* PHP_IO_GENERIC_H */ diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h index 74d9b7d08541b..b00b45adf47a9 100644 --- a/main/io/php_io_internal.h +++ b/main/io/php_io_internal.h @@ -20,9 +20,19 @@ /* Internal utility functions */ zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied); -/* Single registration functions - implemented once per platform file */ -void php_io_register_copy(php_io_copy_ops *ops, uint32_t *capabilities); -void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities); - +/* Platform-specific headers */ +#ifdef __linux__ +#include "php_io_linux.h" +#elif defined(PHP_WIN32) +#include "php_io_windows.h" +#elif defined(__APPLE__) +#include "php_io_macos.h" +#elif defined(__sun) +#include "php_io_solaris.h" +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#include "php_io_bsd.h" +#else +#include "php_io_generic.h" +#endif #endif /* PHP_IO_INTERNAL_H */ diff --git a/main/io/php_io_linux.h b/main/io/php_io_linux.h new file mode 100644 index 0000000000000..651a146ac6d50 --- /dev/null +++ b/main/io/php_io_linux.h @@ -0,0 +1,33 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_LINUX_H +#define PHP_IO_LINUX_H + +/* Copy operations */ +zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_linux_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_linux_copy_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied); + +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_linux_copy_file_to_file, \ + .file_to_socket = php_io_linux_copy_file_to_socket, \ + .socket_to_fd = php_io_linux_copy_socket_to_fd, \ + } + +#define PHP_IO_PLATFORM_NAME "linux" + +#endif /* PHP_IO_LINUX_H */ diff --git a/main/io/php_io_macos.h b/main/io/php_io_macos.h new file mode 100644 index 0000000000000..29260889d95b0 --- /dev/null +++ b/main/io/php_io_macos.h @@ -0,0 +1,32 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_MACOS_H +#define PHP_IO_MACOS_H + +/* Copy operations */ +zend_result php_io_macos_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_macos_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); + +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_macos_copy_file_to_file, \ + .file_to_socket = php_io_macos_copy_file_to_socket, \ + .socket_to_fd = php_io_generic_copy_fallback, \ + } + +#define PHP_IO_PLATFORM_NAME "macos" + +#endif /* PHP_IO_MACOS_H */ diff --git a/main/io/php_io_ring_linux.c b/main/io/php_io_ring_linux.c deleted file mode 100644 index d44b3a80693d2..0000000000000 --- a/main/io/php_io_ring_linux.c +++ /dev/null @@ -1,168 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | Copyright (c) The PHP Group | - +----------------------------------------------------------------------+ - | This source file is subject to version 3.01 of the PHP license, | - | that is bundled with this package in the file LICENSE, and is | - | available through the world-wide-web at the following url: | - | https://www.php.net/license/3_01.txt | - | If you did not receive a copy of the PHP license and are unable to | - | obtain it through the world-wide-web, please send a note to | - | license@php.net so we can mail you a copy immediately. | - +----------------------------------------------------------------------+ - | Authors: PHP Development Team | - +----------------------------------------------------------------------+ -*/ - -#ifdef __linux__ - -#include "php_io_internal.h" - -#ifdef HAVE_IO_URING -#include -#include - -typedef struct { - struct io_uring ring; -} php_io_ring_linux; - -static php_io_ring* php_io_ring_linux_create(int queue_depth) -{ - php_io_ring_linux *lr = emalloc(sizeof(php_io_ring_linux)); - - if (io_uring_queue_init(queue_depth, &lr->ring, 0) == 0) { - return (php_io_ring*)lr; - } - - efree(lr); - return NULL; -} - -static zend_result php_io_ring_linux_submit(php_io_ring *ring, php_io_sqe *sqe) -{ - php_io_ring_linux *lr = (php_io_ring_linux*)ring; - struct io_uring_sqe *uring_sqe = io_uring_get_sqe(&lr->ring); - - if (!uring_sqe) { - return FAILURE; - } - - uring_sqe->user_data = sqe->user_data; - - switch (sqe->op) { - case PHP_IO_OP_SPLICE: - io_uring_prep_splice(uring_sqe, sqe->fd, sqe->params.splice.src_offset, - sqe->params.splice.dest_fd, sqe->params.splice.dest_offset, - sqe->params.splice.len, 0); - break; - case PHP_IO_OP_READ: - io_uring_prep_read(uring_sqe, sqe->fd, sqe->params.rw.buf, - sqe->params.rw.len, sqe->params.rw.offset); - break; - case PHP_IO_OP_WRITE: - io_uring_prep_write(uring_sqe, sqe->fd, sqe->params.rw.buf, - sqe->params.rw.len, sqe->params.rw.offset); - break; - default: - return FAILURE; - } - - return io_uring_submit(&lr->ring) > 0 ? SUCCESS : FAILURE; -} - -static int php_io_ring_linux_wait_cqe(php_io_ring *ring, php_io_cqe **cqe) -{ - php_io_ring_linux *lr = (php_io_ring_linux*)ring; - struct io_uring_cqe *uring_cqe; - static php_io_cqe php_cqe; - - int ret = io_uring_wait_cqe(&lr->ring, &uring_cqe); - if (ret == 0) { - php_cqe.user_data = uring_cqe->user_data; - php_cqe.result = uring_cqe->res; - php_cqe.flags = uring_cqe->flags; - *cqe = &php_cqe; - return 1; - } - - return 0; -} - -static void php_io_ring_linux_cqe_seen(php_io_ring *ring, php_io_cqe *cqe) -{ - php_io_ring_linux *lr = (php_io_ring_linux*)ring; - struct io_uring_cqe *uring_cqe; - - /* Find the corresponding io_uring cqe and mark it as seen */ - if (io_uring_peek_cqe(&lr->ring, &uring_cqe) == 0) { - if (uring_cqe->user_data == cqe->user_data) { - io_uring_cqe_seen(&lr->ring, uring_cqe); - } - } -} - -static void php_io_ring_linux_destroy(php_io_ring *ring) -{ - php_io_ring_linux *lr = (php_io_ring_linux*)ring; - io_uring_queue_exit(&lr->ring); - efree(lr); -} - -/* Check if io_uring is available and supports necessary features */ -static zend_bool php_io_ring_linux_detect_support(void) -{ - struct utsname uts; - if (uname(&uts) != 0) { - return 0; - } - - int major, minor, patch; - if (sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch) != 3) { - return 0; - } - - /* Require kernel 5.7+ for reliable splice support */ - int version = major * 1000000 + minor * 1000 + patch; - if (version < 5007000) { - return 0; - } - - /* Test if io_uring actually works */ - struct io_uring test_ring; - if (io_uring_queue_init(1, &test_ring, 0) == 0) { - io_uring_queue_exit(&test_ring); - return 1; - } - - return 0; -} - -#endif /* HAVE_IO_URING */ - -void php_io_register_ring(php_io_ring_ops *ops, uint32_t *capabilities) -{ -#ifdef HAVE_IO_URING - if (php_io_ring_linux_detect_support()) { - ops->create = php_io_ring_linux_create; - ops->submit = php_io_ring_linux_submit; - ops->wait_cqe = php_io_ring_linux_wait_cqe; - ops->cqe_seen = php_io_ring_linux_cqe_seen; - ops->destroy = php_io_ring_linux_destroy; - ops->capabilities = PHP_IO_RING_CAP_SPLICE | PHP_IO_RING_CAP_READ | - PHP_IO_RING_CAP_WRITE | PHP_IO_RING_CAP_BATCH; - - *capabilities |= PHP_IO_CAP_RING_SUPPORT; - return; - } -#endif - - /* No ring support available */ - ops->create = NULL; - ops->submit = NULL; - ops->wait_cqe = NULL; - ops->cqe_seen = NULL; - ops->destroy = NULL; - ops->capabilities = 0; -} - -#endif /* __linux__ */ diff --git a/main/io/php_io_solaris.h b/main/io/php_io_solaris.h new file mode 100644 index 0000000000000..5b384dd44ba8a --- /dev/null +++ b/main/io/php_io_solaris.h @@ -0,0 +1,31 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_SOLARIS_H +#define PHP_IO_SOLARIS_H + +/* Copy operations */ +zend_result php_io_solaris_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); + +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_generic_copy_fallback, \ + .file_to_socket = php_io_solaris_copy_file_to_socket, \ + .socket_to_fd = php_io_generic_copy_fallback, \ + } + +#define PHP_IO_PLATFORM_NAME "solaris" + +#endif /* PHP_IO_SOLARIS_H */ diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h new file mode 100644 index 0000000000000..d6d3d5cde0709 --- /dev/null +++ b/main/io/php_io_windows.h @@ -0,0 +1,32 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_WINDOWS_H +#define PHP_IO_WINDOWS_H + +/* Copy operations */ +zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_windows_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); + +/* Instance initialization macros */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = php_io_windows_copy_file_to_file, \ + .file_to_socket = php_io_windows_copy_file_to_socket, \ + .socket_to_fd = php_io_generic_copy_fallback, \ + } + +#define PHP_IO_PLATFORM_NAME "windows" + +#endif /* PHP_IO_WINDOWS_H */ diff --git a/main/php_io.h b/main/php_io.h index 17b25d24bea2c..f5b7ac654075e 100644 --- a/main/php_io.h +++ b/main/php_io.h @@ -19,14 +19,6 @@ /* Forward declarations */ typedef struct php_io php_io; -typedef struct php_io_ring php_io_ring; - -/* Operation types for ring API */ -typedef enum { - PHP_IO_OP_READ, - PHP_IO_OP_WRITE, - PHP_IO_OP_SPLICE, -} php_io_op_type; /* File descriptor types */ typedef enum { @@ -36,53 +28,6 @@ typedef enum { PHP_IO_FD_UNKNOWN, } php_io_fd_type; -/* Ring-specific capabilities */ -#define PHP_IO_RING_CAP_SPLICE (1 << 0) -#define PHP_IO_RING_CAP_READ (1 << 1) -#define PHP_IO_RING_CAP_WRITE (1 << 2) -#define PHP_IO_RING_CAP_BATCH (1 << 3) - -/* General I/O capabilities */ -#define PHP_IO_CAP_RING_SUPPORT (1 << 0) -#define PHP_IO_CAP_ZERO_COPY (1 << 1) -#define PHP_IO_CAP_SENDFILE (1 << 2) - -/* Ring API structures */ -typedef struct php_io_sqe { - php_io_op_type op; - uint64_t user_data; - int fd; - union { - struct { - int dest_fd; - size_t len; - off_t src_offset; - off_t dest_offset; - } splice; - struct { - void *buf; - size_t len; - off_t offset; - } rw; - } params; -} php_io_sqe; - -typedef struct php_io_cqe { - uint64_t user_data; - ssize_t result; - uint32_t flags; -} php_io_cqe; - -/* Ring operations vtable */ -typedef struct php_io_ring_ops { - php_io_ring* (*create)(int queue_depth); - zend_result (*submit)(php_io_ring *ring, php_io_sqe *sqe); - int (*wait_cqe)(php_io_ring *ring, php_io_cqe **cqe); - void (*cqe_seen)(php_io_ring *ring, php_io_cqe *cqe); - void (*destroy)(php_io_ring *ring); - uint32_t capabilities; -} php_io_ring_ops; - /* Synchronous copy operations vtable */ typedef struct php_io_copy_ops { zend_result (*file_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); @@ -93,52 +38,13 @@ typedef struct php_io_copy_ops { /* Main php_io structure */ typedef struct php_io { php_io_copy_ops copy; - php_io_ring_ops ring; const char *platform_name; - uint32_t capabilities; } php_io; -/* Factory functions */ -PHPAPI php_io* php_io_create(void); +/* Accessor function for global io instance */ PHPAPI php_io* php_io_get(void); /* Utility functions */ PHPAPI php_io_fd_type php_io_detect_fd_type(int fd); -PHPAPI zend_result php_io_get_fd_and_type(php_stream *stream, int *fd, php_io_fd_type *type); - -/* Ring API helper functions */ -static inline void php_io_ring_prep_splice(php_io_sqe *sqe, uint64_t user_data, - int src_fd, int dest_fd, size_t len) -{ - sqe->op = PHP_IO_OP_SPLICE; - sqe->user_data = user_data; - sqe->fd = src_fd; - sqe->params.splice.dest_fd = dest_fd; - sqe->params.splice.len = len; - sqe->params.splice.src_offset = 0; - sqe->params.splice.dest_offset = 0; -} - -static inline void php_io_ring_prep_read(php_io_sqe *sqe, uint64_t user_data, - int fd, void *buf, size_t len, off_t offset) -{ - sqe->op = PHP_IO_OP_READ; - sqe->user_data = user_data; - sqe->fd = fd; - sqe->params.rw.buf = buf; - sqe->params.rw.len = len; - sqe->params.rw.offset = offset; -} - -static inline void php_io_ring_prep_write(php_io_sqe *sqe, uint64_t user_data, - int fd, void *buf, size_t len, off_t offset) -{ - sqe->op = PHP_IO_OP_WRITE; - sqe->user_data = user_data; - sqe->fd = fd; - sqe->params.rw.buf = buf; - sqe->params.rw.len = len; - sqe->params.rw.offset = offset; -} #endif /* PHP_IO_H */ From 0f3a459e4c005e4071e7b5eba90e499899018df9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Nov 2025 23:00:37 +0100 Subject: [PATCH 03/10] io: refactore fd types and add io copy function --- main/io/php_io.c | 35 ++++++++++++++++++++++------------- main/io/php_io_bsd.h | 7 ++++--- main/io/php_io_copy_bsd.c | 2 +- main/io/php_io_copy_linux.c | 11 +++++------ main/io/php_io_copy_macos.c | 14 +------------- main/io/php_io_copy_solaris.c | 2 +- main/io/php_io_copy_windows.c | 6 +++--- main/io/php_io_generic.h | 7 ++++--- main/io/php_io_linux.h | 11 ++++++----- main/io/php_io_macos.h | 10 +++++----- main/io/php_io_solaris.h | 8 +++++--- main/io/php_io_windows.h | 8 +++++--- main/php_io.h | 25 ++++++++++++++----------- 13 files changed, 76 insertions(+), 70 deletions(-) diff --git a/main/io/php_io.c b/main/io/php_io.c index c674272302d08..410dfeb5a2dc8 100644 --- a/main/io/php_io.c +++ b/main/io/php_io.c @@ -16,7 +16,6 @@ #include "php_io.h" #include "php_io_internal.h" #include -#include #ifdef PHP_WIN32 #include @@ -43,25 +42,35 @@ PHPAPI php_io_fd_type php_io_detect_fd_type(int fd) struct stat st; if (fstat(fd, &st) != 0) { - return PHP_IO_FD_UNKNOWN; + /* Can't stat - assume generic (safest fallback) */ + return PHP_IO_FD_GENERIC; } if (S_ISREG(st.st_mode)) { return PHP_IO_FD_FILE; - } else if (S_ISSOCK(st.st_mode)) { - return PHP_IO_FD_SOCKET; - } else if (S_ISFIFO(st.st_mode)) { - return PHP_IO_FD_PIPE; } - /* Additional socket detection for systems where S_ISSOCK doesn't work */ - int sock_type; - socklen_t sock_type_len = sizeof(sock_type); - if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &sock_type, &sock_type_len) == 0) { - return PHP_IO_FD_SOCKET; - } + /* Everything else (socket, pipe, fifo, char device, etc.) is generic */ + return PHP_IO_FD_GENERIC; +} - return PHP_IO_FD_UNKNOWN; +/* High-level copy function with dispatch */ +PHPAPI zend_result php_io_copy(int src_fd, php_io_fd_type src_type, int dest_fd, + php_io_fd_type dest_type, size_t len, size_t *copied) +{ + php_io *io = php_io_get(); + + /* Dispatch to appropriate copy function based on fd types */ + if (src_type == PHP_IO_FD_FILE && dest_type == PHP_IO_FD_FILE) { + return io->copy.file_to_file(src_fd, dest_fd, len, copied); + } else if (src_type == PHP_IO_FD_FILE && dest_type == PHP_IO_FD_GENERIC) { + return io->copy.file_to_generic(src_fd, dest_fd, len, copied); + } else if (src_type == PHP_IO_FD_GENERIC && dest_type == PHP_IO_FD_FILE) { + return io->copy.generic_to_file(src_fd, dest_fd, len, copied); + } else { + /* generic to generic */ + return io->copy.generic_to_generic(src_fd, dest_fd, len, copied); + } } /* Generic read/write fallback implementation */ diff --git a/main/io/php_io_bsd.h b/main/io/php_io_bsd.h index 2f62fc61dcd18..cac6b5b91e33e 100644 --- a/main/io/php_io_bsd.h +++ b/main/io/php_io_bsd.h @@ -16,14 +16,15 @@ #define PHP_IO_BSD_H /* Copy operations */ -zend_result php_io_bsd_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_generic_copy_fallback, \ - .file_to_socket = php_io_bsd_copy_file_to_socket, \ - .socket_to_fd = php_io_generic_copy_fallback, \ + .file_to_generic = php_io_bsd_copy_file_to_generic, \ + .generic_to_file = php_io_generic_copy_fallback, \ + .generic_to_generic = php_io_generic_copy_fallback, \ } #define PHP_IO_PLATFORM_NAME "bsd" diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c index 196a570a4290a..9b95a0c9132cf 100644 --- a/main/io/php_io_copy_bsd.c +++ b/main/io/php_io_copy_bsd.c @@ -23,7 +23,7 @@ #include #endif -zend_result php_io_bsd_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILE /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index d9c66ea02f8d3..daee66e6452fe 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -71,10 +71,10 @@ zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -zend_result php_io_linux_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILE - /* Use sendfile for zero-copy file to socket transfer */ + /* Use sendfile for zero-copy file to socket/pipe transfer */ off_t offset = 0; ssize_t result = sendfile(dest_fd, src_fd, &offset, len); @@ -97,16 +97,15 @@ zend_result php_io_linux_copy_file_to_socket(int src_fd, int dest_fd, size_t len return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -zend_result php_io_linux_copy_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SPLICE - /* Use splice for zero-copy socket to fd transfer */ - /* splice() can work directly between socket and file/pipe */ + /* Use splice for zero-copy transfer from socket/pipe to any fd */ + /* splice() works for: socket→file, socket→pipe, pipe→file, pipe→socket, etc. */ size_t total_copied = 0; size_t remaining = len; while (remaining > 0) { - /* splice from socket to destination fd */ ssize_t result = splice(src_fd, NULL, dest_fd, NULL, remaining, SPLICE_F_MOVE | SPLICE_F_MORE); diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c index 6064b13392375..1ad1ab7f06f97 100644 --- a/main/io/php_io_copy_macos.c +++ b/main/io/php_io_copy_macos.c @@ -27,19 +27,7 @@ #include #endif -zend_result php_io_macos_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) -{ -#ifdef HAVE_COPYFILE - /* macOS copyfile() can be used, but it's designed for whole files */ - /* For partial copies, it's complex to use properly */ - /* TODO: Could implement copyfile() for whole-file copies in the future */ -#endif - - /* For now, use generic implementation for file-to-file */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); -} - -zend_result php_io_macos_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILE /* macOS sendfile signature: sendfile(fd, s, offset, len, hdtr, flags) */ diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c index 6e3bebc215864..e1353ffe64519 100644 --- a/main/io/php_io_copy_solaris.c +++ b/main/io/php_io_copy_solaris.c @@ -22,7 +22,7 @@ #include #endif -zend_result php_io_solaris_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) { #ifdef HAVE_SENDFILEV /* Solaris sendfilev - very powerful but complex API */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c index 146d7ed35adb9..155514892e09e 100644 --- a/main/io/php_io_copy_windows.c +++ b/main/io/php_io_copy_windows.c @@ -22,7 +22,7 @@ zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) { - /* Try CopyFileEx for optimal file-to-file copying */ + /* Use ReadFile/WriteFile for file-to-file copying */ HANDLE src_handle = (HANDLE) _get_osfhandle(src_fd); HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); @@ -32,7 +32,7 @@ zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len if (GetFileSizeEx(src_handle, &file_size)) { DWORD bytes_to_copy = (DWORD) min(len, (size_t) file_size.QuadPart); - /* Use ReadFile/WriteFile for partial copies since CopyFileEx copies entire files */ + /* Use ReadFile/WriteFile for partial copies */ char buffer[65536]; DWORD total_copied = 0; @@ -68,7 +68,7 @@ zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); } -zend_result php_io_windows_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied) +zend_result php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) { /* Use TransmitFile for zero-copy file to socket transfer */ HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); diff --git a/main/io/php_io_generic.h b/main/io/php_io_generic.h index cc6968f0913c7..f33995dcae7ff 100644 --- a/main/io/php_io_generic.h +++ b/main/io/php_io_generic.h @@ -15,12 +15,13 @@ #ifndef PHP_IO_GENERIC_H #define PHP_IO_GENERIC_H -/* Instance initialization macros */ +/* Instance initialization macros - all use the generic fallback */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_generic_copy_fallback, \ - .file_to_socket = php_io_generic_copy_fallback, \ - .socket_to_fd = php_io_generic_copy_fallback, \ + .file_to_generic = php_io_generic_copy_fallback, \ + .generic_to_file = php_io_generic_copy_fallback, \ + .generic_to_generic = php_io_generic_copy_fallback, \ } #define PHP_IO_PLATFORM_NAME "generic" diff --git a/main/io/php_io_linux.h b/main/io/php_io_linux.h index 651a146ac6d50..aceeff0f8be00 100644 --- a/main/io/php_io_linux.h +++ b/main/io/php_io_linux.h @@ -17,17 +17,18 @@ /* Copy operations */ zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_linux_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_linux_copy_socket_to_fd(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t len, size_t *copied); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_linux_copy_file_to_file, \ - .file_to_socket = php_io_linux_copy_file_to_socket, \ - .socket_to_fd = php_io_linux_copy_socket_to_fd, \ + .file_to_generic = php_io_linux_copy_file_to_generic, \ + .generic_to_file = php_io_linux_copy_generic_to_any, \ + .generic_to_generic = php_io_linux_copy_generic_to_any, \ } #define PHP_IO_PLATFORM_NAME "linux" -#endif /* PHP_IO_LINUX_H */ +#endif /* PHP_IO_LINUX_H */ \ No newline at end of file diff --git a/main/io/php_io_macos.h b/main/io/php_io_macos.h index 29260889d95b0..8b216a4ee5d34 100644 --- a/main/io/php_io_macos.h +++ b/main/io/php_io_macos.h @@ -16,15 +16,15 @@ #define PHP_IO_MACOS_H /* Copy operations */ -zend_result php_io_macos_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_macos_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ - .file_to_file = php_io_macos_copy_file_to_file, \ - .file_to_socket = php_io_macos_copy_file_to_socket, \ - .socket_to_fd = php_io_generic_copy_fallback, \ + .file_to_file = php_io_generic_copy_fallback, \ + .file_to_generic = php_io_macos_copy_file_to_generic, \ + .generic_to_file = php_io_generic_copy_fallback, \ + .generic_to_generic = php_io_generic_copy_fallback, \ } #define PHP_IO_PLATFORM_NAME "macos" diff --git a/main/io/php_io_solaris.h b/main/io/php_io_solaris.h index 5b384dd44ba8a..873b94c9e4882 100644 --- a/main/io/php_io_solaris.h +++ b/main/io/php_io_solaris.h @@ -16,14 +16,16 @@ #define PHP_IO_SOLARIS_H /* Copy operations */ -zend_result php_io_solaris_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_solaris_copy_file_to_generic( + int src_fd, int dest_fd, size_t len, size_t *copied); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_generic_copy_fallback, \ - .file_to_socket = php_io_solaris_copy_file_to_socket, \ - .socket_to_fd = php_io_generic_copy_fallback, \ + .file_to_generic = php_io_solaris_copy_file_to_generic, \ + .generic_to_file = php_io_generic_copy_fallback, \ + .generic_to_generic = php_io_generic_copy_fallback, \ } #define PHP_IO_PLATFORM_NAME "solaris" diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h index d6d3d5cde0709..4405c6ba5d418 100644 --- a/main/io/php_io_windows.h +++ b/main/io/php_io_windows.h @@ -17,14 +17,16 @@ /* Copy operations */ zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_windows_copy_file_to_socket(int src_fd, int dest_fd, size_t len, size_t *copied); +zend_result php_io_windows_copy_file_to_generic( + int src_fd, int dest_fd, size_t len, size_t *copied); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_windows_copy_file_to_file, \ - .file_to_socket = php_io_windows_copy_file_to_socket, \ - .socket_to_fd = php_io_generic_copy_fallback, \ + .file_to_generic = php_io_windows_copy_file_to_generic, \ + .generic_to_file = php_io_generic_copy_fallback, \ + .generic_to_generic = php_io_generic_copy_fallback, \ } #define PHP_IO_PLATFORM_NAME "windows" diff --git a/main/php_io.h b/main/php_io.h index f5b7ac654075e..d56ef1dd7c1ba 100644 --- a/main/php_io.h +++ b/main/php_io.h @@ -22,29 +22,32 @@ typedef struct php_io php_io; /* File descriptor types */ typedef enum { - PHP_IO_FD_FILE, - PHP_IO_FD_SOCKET, - PHP_IO_FD_PIPE, - PHP_IO_FD_UNKNOWN, + PHP_IO_FD_FILE, /* Regular file - can use optimized file operations */ + PHP_IO_FD_GENERIC, /* Socket, pipe, or other - use generic operations */ } php_io_fd_type; /* Synchronous copy operations vtable */ typedef struct php_io_copy_ops { - zend_result (*file_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); - zend_result (*file_to_socket)(int src_fd, int dest_fd, size_t len, size_t *copied); - zend_result (*socket_to_fd)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*file_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*file_to_generic)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*generic_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); + zend_result (*generic_to_generic)(int src_fd, int dest_fd, size_t len, size_t *copied); } php_io_copy_ops; /* Main php_io structure */ typedef struct php_io { - php_io_copy_ops copy; - const char *platform_name; + php_io_copy_ops copy; + const char *platform_name; } php_io; -/* Accessor function for global io instance */ -PHPAPI php_io* php_io_get(void); +/* IO struct accessor functions */ +PHPAPI php_io *php_io_get(void); /* Utility functions */ PHPAPI php_io_fd_type php_io_detect_fd_type(int fd); +/* High-level copy function - automatically selects best method based on fd types */ +PHPAPI zend_result php_io_copy(int src_fd, php_io_fd_type src_type, int dest_fd, + php_io_fd_type dest_type, size_t len, size_t *copied); + #endif /* PHP_IO_H */ From 9b52e5b946d25c1612ba8d985598390cbfc02d65 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 16:28:54 +0100 Subject: [PATCH 04/10] io: modify and use io api in stream copy and add build --- configure.ac | 12 ++ main/io/php_io.c | 64 +++++------ main/io/php_io_bsd.h | 4 +- main/io/php_io_copy_bsd.c | 79 ++++++++----- main/io/php_io_copy_linux.c | 178 ++++++++++++++++++----------- main/io/php_io_copy_macos.c | 81 ++++++++----- main/io/php_io_copy_solaris.c | 99 ++++++++-------- main/io/php_io_copy_windows.c | 72 ++++++------ main/io/php_io_generic.h | 2 +- main/io/php_io_internal.h | 2 +- main/io/php_io_linux.h | 6 +- main/io/php_io_macos.h | 4 +- main/io/php_io_solaris.h | 5 +- main/io/php_io_windows.h | 7 +- main/php_io.h | 29 +++-- main/streams/streams.c | 207 +++++++++------------------------- win32/build/config.w32 | 3 + 17 files changed, 436 insertions(+), 418 deletions(-) diff --git a/configure.ac b/configure.ac index 77fc8c89cdf40..24d7d981c178f 100644 --- a/configure.ac +++ b/configure.ac @@ -580,10 +580,13 @@ AC_CHECK_FUNCS(m4_normalize([ putenv reallocarray scandir + sendfile + sendfilev setenv setitimer shutdown sigprocmask + splice statfs statvfs std_syslog @@ -1688,6 +1691,15 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/io], m4_normalize([ + php_io.c + php_io_copy_bsd.c + php_io_copy_linux.c + php_io_copy_macos.c + php_io_copy_solaris.c + ]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c diff --git a/main/io/php_io.c b/main/io/php_io.c index 410dfeb5a2dc8..675a3b858eee7 100644 --- a/main/io/php_io.c +++ b/main/io/php_io.c @@ -36,71 +36,63 @@ PHPAPI php_io *php_io_get(void) return &php_io_instance; } -/* Detect file descriptor type */ -PHPAPI php_io_fd_type php_io_detect_fd_type(int fd) -{ - struct stat st; - - if (fstat(fd, &st) != 0) { - /* Can't stat - assume generic (safest fallback) */ - return PHP_IO_FD_GENERIC; - } - - if (S_ISREG(st.st_mode)) { - return PHP_IO_FD_FILE; - } - - /* Everything else (socket, pipe, fifo, char device, etc.) is generic */ - return PHP_IO_FD_GENERIC; -} - /* High-level copy function with dispatch */ -PHPAPI zend_result php_io_copy(int src_fd, php_io_fd_type src_type, int dest_fd, - php_io_fd_type dest_type, size_t len, size_t *copied) +PHPAPI ssize_t php_io_copy( + int src_fd, php_io_fd_type src_type, int dest_fd, php_io_fd_type dest_type, size_t maxlen) { php_io *io = php_io_get(); /* Dispatch to appropriate copy function based on fd types */ if (src_type == PHP_IO_FD_FILE && dest_type == PHP_IO_FD_FILE) { - return io->copy.file_to_file(src_fd, dest_fd, len, copied); + return io->copy.file_to_file(src_fd, dest_fd, maxlen); } else if (src_type == PHP_IO_FD_FILE && dest_type == PHP_IO_FD_GENERIC) { - return io->copy.file_to_generic(src_fd, dest_fd, len, copied); + return io->copy.file_to_generic(src_fd, dest_fd, maxlen); } else if (src_type == PHP_IO_FD_GENERIC && dest_type == PHP_IO_FD_FILE) { - return io->copy.generic_to_file(src_fd, dest_fd, len, copied); + return io->copy.generic_to_file(src_fd, dest_fd, maxlen); } else { /* generic to generic */ - return io->copy.generic_to_generic(src_fd, dest_fd, len, copied); + return io->copy.generic_to_generic(src_fd, dest_fd, maxlen); } } /* Generic read/write fallback implementation */ -zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen) { char buf[8192]; size_t total_copied = 0; - size_t remaining = len; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - while (remaining > 0 && total_copied < len) { - size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); + while (remaining > 0) { + size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); ssize_t bytes_read = read(src_fd, buf, to_read); - if (bytes_read <= 0) { - break; /* EOF or error */ + if (bytes_read < 0) { + /* Read error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } else if (bytes_read == 0) { + /* EOF reached */ + return (ssize_t) total_copied; } ssize_t bytes_written = write(dest_fd, buf, bytes_read); - if (bytes_written <= 0) { - break; /* Error */ + if (bytes_written < 0) { + /* Write error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } else if (bytes_written == 0) { + /* Couldn't write anything */ + return total_copied > 0 ? (ssize_t) total_copied : -1; } total_copied += bytes_written; - remaining -= bytes_written; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_written; + } if (bytes_written != bytes_read) { - break; /* Partial write */ + /* Partial write - stop here */ + return (ssize_t) total_copied; } } - *copied = total_copied; - return total_copied > 0 ? SUCCESS : FAILURE; + return (ssize_t) total_copied; } diff --git a/main/io/php_io_bsd.h b/main/io/php_io_bsd.h index cac6b5b91e33e..352fa801e9a51 100644 --- a/main/io/php_io_bsd.h +++ b/main/io/php_io_bsd.h @@ -16,7 +16,7 @@ #define PHP_IO_BSD_H /* Copy operations */ -zend_result php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ @@ -29,4 +29,4 @@ zend_result php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t len, #define PHP_IO_PLATFORM_NAME "bsd" -#endif /* PHP_IO_BSD_H */ +#endif /* PHP_IO_BSD_H */ \ No newline at end of file diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c index 9b95a0c9132cf..89c1f80ffa51e 100644 --- a/main/io/php_io_copy_bsd.c +++ b/main/io/php_io_copy_bsd.c @@ -23,44 +23,65 @@ #include #endif -zend_result php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_SENDFILE /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ - /* This signature is shared by FreeBSD, OpenBSD, and NetBSD */ - off_t sbytes = 0; - int result = sendfile(src_fd, dest_fd, 0, len, NULL, &sbytes, 0); + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - if (result == 0) { - /* Success - entire amount was sent */ - *copied = len; - return SUCCESS; - } else if (result == -1 && sbytes > 0) { - /* Partial send - some data was transferred */ - *copied = sbytes; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - case EBUSY: - /* Would block or busy - could retry, but fall back for now */ - break; - case EINVAL: - /* Invalid arguments - likely not a regular file or socket */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - default: - /* Other errors */ - break; + while (remaining > 0) { + off_t to_send = (remaining < OFF_MAX) ? (off_t) remaining : OFF_MAX; + off_t sbytes = 0; + int result = sendfile(src_fd, dest_fd, 0, to_send, NULL, &sbytes, 0); + + if (result == 0 || sbytes > 0) { + /* Success or partial send */ + total_copied += sbytes; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= sbytes; + } + + /* If result == 0, entire amount was sent, continue if needed */ + /* If result == -1 but sbytes > 0, partial send occurred */ + if (result == -1 || sbytes < to_send) { + return (ssize_t) total_copied; + } + } else { + /* Error occurred with no data transferred */ + switch (errno) { + case EAGAIN: + case EBUSY: + case EINVAL: + case ENOTCONN: + /* Various errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + default: + /* Other errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + } + break; + } + + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; } } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } #endif /* HAVE_SENDFILE */ /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } #endif /* FreeBSD, OpenBSD, NetBSD */ diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index daee66e6452fe..fd239b1498b4b 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -40,130 +40,178 @@ static ssize_t copy_file_range_wrapper( } #endif -zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_COPY_FILE_RANGE - /* Try copy_file_range first - kernel-level optimization */ - ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, len, 0); + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - if (result > 0) { - *copied = (size_t) result; - return SUCCESS; - } + while (remaining > 0) { + /* Clamp to SSIZE_MAX to avoid issues */ + size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, to_copy, 0); + + if (result > 0) { + total_copied += result; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= result; + } + + /* If we got less than requested, we likely hit EOF */ + if ((size_t) result < to_copy) { + return (ssize_t) total_copied; + } + } else if (result == 0) { + /* EOF */ + return (ssize_t) total_copied; + } else { + /* Error occurred */ + switch (errno) { + case EINVAL: + case EXDEV: + case ENOSYS: + /* Expected failures - fall back to generic copy */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + default: + /* Unexpected error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + break; + } - /* If copy_file_range fails, fall through to generic implementation */ - if (result == -1) { - switch (errno) { - case EINVAL: - case EXDEV: - case ENOSYS: - /* Expected failures - fall back to generic copy */ - break; - default: - /* Unexpected error */ - *copied = 0; - return FAILURE; + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; } } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } #endif /* Fallback to generic read/write loop */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } -zend_result php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_SENDFILE - /* Use sendfile for zero-copy file to socket/pipe transfer */ - off_t offset = 0; - ssize_t result = sendfile(dest_fd, src_fd, &offset, len); + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - if (result > 0) { - *copied = (size_t) result; - return SUCCESS; - } + while (remaining > 0) { + /* Clamp to SSIZE_MAX */ + size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + ssize_t result = sendfile(dest_fd, src_fd, NULL, to_send); - /* Handle partial sends and errors */ - if (result == -1) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - /* Would block - for now, fall back to generic copy */ - /* TODO: Could implement epoll-based retry here */ + if (result > 0) { + total_copied += result; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= result; + } + + /* If we got less than requested, we likely hit EOF or would block */ + if ((size_t) result < to_send) { + return (ssize_t) total_copied; + } + } else if (result == 0) { + /* EOF */ + return (ssize_t) total_copied; + } else { + /* Error occurred */ + if (errno == EAGAIN) { + /* Would block - return what we have */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + /* Other errors - fall back if we haven't copied anything yet */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + } + + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; } - /* Other errors fall through to generic copy */ + } + + if (total_copied > 0) { + return (ssize_t) total_copied; } #endif /* Fallback to generic read/write loop */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } -zend_result php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_SPLICE - /* Use splice for zero-copy transfer from socket/pipe to any fd */ - /* splice() works for: socket→file, socket→pipe, pipe→file, pipe→socket, etc. */ size_t total_copied = 0; - size_t remaining = len; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; while (remaining > 0) { + /* Clamp to SSIZE_MAX */ + size_t to_splice = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; ssize_t result - = splice(src_fd, NULL, dest_fd, NULL, remaining, SPLICE_F_MOVE | SPLICE_F_MORE); + = splice(src_fd, NULL, dest_fd, NULL, to_splice, SPLICE_F_MOVE | SPLICE_F_MORE); if (result > 0) { total_copied += result; - remaining -= result; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= result; + } + + /* If we got less than requested, we likely hit EOF or would block */ + if ((size_t) result < to_splice) { + return (ssize_t) total_copied; + } } else if (result == 0) { /* EOF */ - break; + return (ssize_t) total_copied; } else { /* Error occurred */ switch (errno) { case EAGAIN: - case EWOULDBLOCK: - /* Would block - return what we've copied so far */ - if (total_copied > 0) { - *copied = total_copied; - return SUCCESS; - } - /* Fall through to generic if nothing copied yet */ - break; + /* Would block */ + return total_copied > 0 ? (ssize_t) total_copied : -1; case EINVAL: /* splice not supported for these fds */ if (total_copied > 0) { - /* We already copied some data, return success */ - *copied = total_copied; - return SUCCESS; + return (ssize_t) total_copied; } - /* Fall through to generic */ break; case EPIPE: /* Broken pipe */ - if (total_copied > 0) { - *copied = total_copied; - return SUCCESS; - } - *copied = 0; - return FAILURE; + return total_copied > 0 ? (ssize_t) total_copied : -1; default: /* Other errors */ if (total_copied > 0) { - *copied = total_copied; - return SUCCESS; + return (ssize_t) total_copied; } break; } break; } + + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; + } } if (total_copied > 0) { - *copied = total_copied; - return SUCCESS; + return (ssize_t) total_copied; } #endif /* HAVE_SPLICE */ /* Fallback to generic read/write loop */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } #endif /* __linux__ */ diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c index 1ad1ab7f06f97..973b3e808de6a 100644 --- a/main/io/php_io_copy_macos.c +++ b/main/io/php_io_copy_macos.c @@ -27,46 +27,67 @@ #include #endif -zend_result php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_SENDFILE /* macOS sendfile signature: sendfile(fd, s, offset, len, hdtr, flags) */ /* Note: len is passed by reference and updated with bytes sent */ - off_t len_sent = len; - int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - if (result == 0) { - /* Success */ - *copied = len_sent; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - /* Would block - could be partial send */ - if (len_sent > 0) { - *copied = len_sent; - return SUCCESS; - } - break; - case EINVAL: - /* Invalid arguments - likely not a regular file or socket */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - case EPIPE: - /* Broken pipe */ - break; - default: - /* Other errors */ - break; + while (remaining > 0) { + off_t to_send = (remaining < OFF_MAX) ? (off_t) remaining : OFF_MAX; + off_t len_sent = to_send; + int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); + + if (result == 0 || len_sent > 0) { + /* Success or partial send */ + total_copied += len_sent; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= len_sent; + } + + /* If we got less than requested or result != 0, stop */ + if (len_sent < to_send || result != 0) { + return (ssize_t) total_copied; + } + } else { + /* Error occurred */ + switch (errno) { + case EAGAIN: + /* Would block */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + case EINVAL: + case ENOTCONN: + case EPIPE: + /* Various errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + default: + /* Other errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + } + break; + } + + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; } } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } #endif /* HAVE_SENDFILE */ /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } #endif /* __APPLE__ */ diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c index e1353ffe64519..c1c2232d74ecc 100644 --- a/main/io/php_io_copy_solaris.c +++ b/main/io/php_io_copy_solaris.c @@ -22,63 +22,74 @@ #include #endif -zend_result php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_SENDFILEV /* Solaris sendfilev - very powerful but complex API */ - struct sendfilevec sfv; - size_t xferred = 0; + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - /* Set up the sendfile vector */ - sfv.sfv_fd = src_fd; /* Source file descriptor */ - sfv.sfv_flag = SFV_FD; /* sfv_fd is a file descriptor */ - sfv.sfv_off = 0; /* Offset in the file */ - sfv.sfv_len = len; /* Number of bytes to send */ + while (remaining > 0) { + struct sendfilevec sfv; + size_t xferred = 0; + size_t to_send = (remaining < SIZE_MAX) ? remaining : SIZE_MAX; - /* Perform the sendfile operation */ - ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); + /* Set up the sendfile vector */ + sfv.sfv_fd = src_fd; + sfv.sfv_flag = SFV_FD; + sfv.sfv_off = 0; + sfv.sfv_len = to_send; - if (result == 0) { - /* Success - all data transferred */ - *copied = xferred; - return SUCCESS; - } else if (result == -1) { - /* Error occurred */ - switch (errno) { - case EAGAIN: - /* Would block - partial transfer possible */ - if (xferred > 0) { - *copied = xferred; - return SUCCESS; - } - break; - case EINVAL: - /* Invalid arguments */ - break; - case ENOTCONN: - /* Socket not connected */ - break; - case EPIPE: - /* Broken pipe */ - break; - case EAFNOSUPPORT: - /* Address family not supported */ - break; - default: - /* Other errors */ - break; + /* Perform the sendfile operation */ + ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); + + if (result == 0 || xferred > 0) { + /* Success or partial transfer */ + total_copied += xferred; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= xferred; + } + + /* If we got less than requested or error occurred, stop */ + if (result != 0 || xferred < to_send) { + return (ssize_t) total_copied; + } + } else { + /* Error occurred with no data transferred */ + switch (errno) { + case EAGAIN: + case EINVAL: + case ENOTCONN: + case EPIPE: + case EAFNOSUPPORT: + /* Various errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + default: + /* Other errors */ + if (total_copied > 0) { + return (ssize_t) total_copied; + } + break; + } + break; } - /* Even on error, some data might have been transferred */ - if (xferred > 0) { - *copied = xferred; - return SUCCESS; + /* For bounded copies, stop if we reached maxlen */ + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + return (ssize_t) total_copied; } } + + if (total_copied > 0) { + return (ssize_t) total_copied; + } #endif /* HAVE_SENDFILEV */ /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } #endif /* __sun */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c index 155514892e09e..867ade9be6ef5 100644 --- a/main/io/php_io_copy_windows.c +++ b/main/io/php_io_copy_windows.c @@ -20,55 +20,55 @@ #include #include -zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) { /* Use ReadFile/WriteFile for file-to-file copying */ HANDLE src_handle = (HANDLE) _get_osfhandle(src_fd); HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); if (src_handle != INVALID_HANDLE_VALUE && dest_handle != INVALID_HANDLE_VALUE) { - /* Get source file size to determine copy length */ - LARGE_INTEGER file_size; - if (GetFileSizeEx(src_handle, &file_size)) { - DWORD bytes_to_copy = (DWORD) min(len, (size_t) file_size.QuadPart); + char buffer[65536]; + DWORD total_copied = 0; + DWORD remaining = (maxlen == PHP_IO_COPY_ALL) ? MAXDWORD : (DWORD) min(maxlen, MAXDWORD); - /* Use ReadFile/WriteFile for partial copies */ - char buffer[65536]; - DWORD total_copied = 0; + while (remaining > 0) { + DWORD to_read = min(sizeof(buffer), remaining); + DWORD bytes_read, bytes_written; - while (total_copied < bytes_to_copy) { - DWORD to_read = min(sizeof(buffer), bytes_to_copy - total_copied); - DWORD bytes_read, bytes_written; - - if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { - break; - } - - if (bytes_read == 0) { - break; /* EOF */ - } + if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { + /* Read error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } - if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { - break; - } + if (bytes_read == 0) { + /* EOF */ + return (ssize_t) total_copied; + } - total_copied += bytes_written; + if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { + /* Write error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } - if (bytes_written != bytes_read) { - break; /* Partial write */ - } + total_copied += bytes_written; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_written; } - *copied = total_copied; - return total_copied > 0 ? SUCCESS : FAILURE; + if (bytes_written != bytes_read) { + /* Partial write */ + return (ssize_t) total_copied; + } } + + return (ssize_t) total_copied; } /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } -zend_result php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied) +ssize_t php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) { /* Use TransmitFile for zero-copy file to socket transfer */ HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); @@ -76,24 +76,24 @@ zend_result php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t if (file_handle != INVALID_HANDLE_VALUE && sock != INVALID_SOCKET) { /* TransmitFile can send entire file or partial */ - DWORD bytes_to_send = (DWORD) len; + DWORD bytes_to_send = (maxlen == PHP_IO_COPY_ALL) ? 0 : (DWORD) min(maxlen, MAXDWORD); if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { - *copied = bytes_to_send; - return SUCCESS; + /* TransmitFile succeeded - but we don't know exactly how much was sent without extra + * syscalls */ + /* For simplicity, assume the requested amount was sent */ + return (maxlen == PHP_IO_COPY_ALL) ? 0 : (ssize_t) bytes_to_send; } /* TransmitFile failed, check if it's a recoverable error */ int error = WSAGetLastError(); if (error == WSAENOTSOCK) { /* dest_fd is not a socket, fall back to generic copy */ - } else { - /* Other TransmitFile error, could be network related */ } } /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, len, copied); + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } #endif /* PHP_WIN32 */ diff --git a/main/io/php_io_generic.h b/main/io/php_io_generic.h index f33995dcae7ff..1d512473f3328 100644 --- a/main/io/php_io_generic.h +++ b/main/io/php_io_generic.h @@ -26,4 +26,4 @@ #define PHP_IO_PLATFORM_NAME "generic" -#endif /* PHP_IO_GENERIC_H */ +#endif /* PHP_IO_GENERIC_H */ \ No newline at end of file diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h index b00b45adf47a9..66b1dbf80e59e 100644 --- a/main/io/php_io_internal.h +++ b/main/io/php_io_internal.h @@ -18,7 +18,7 @@ #include "php_io.h" /* Internal utility functions */ -zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen); /* Platform-specific headers */ #ifdef __linux__ diff --git a/main/io/php_io_linux.h b/main/io/php_io_linux.h index aceeff0f8be00..e4b5ade8f11f8 100644 --- a/main/io/php_io_linux.h +++ b/main/io/php_io_linux.h @@ -16,9 +16,9 @@ #define PHP_IO_LINUX_H /* Copy operations */ -zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen); +ssize_t php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); +ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ diff --git a/main/io/php_io_macos.h b/main/io/php_io_macos.h index 8b216a4ee5d34..7ef42f9fa2d2e 100644 --- a/main/io/php_io_macos.h +++ b/main/io/php_io_macos.h @@ -16,7 +16,7 @@ #define PHP_IO_MACOS_H /* Copy operations */ -zend_result php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ @@ -29,4 +29,4 @@ zend_result php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t le #define PHP_IO_PLATFORM_NAME "macos" -#endif /* PHP_IO_MACOS_H */ +#endif /* PHP_IO_MACOS_H */ \ No newline at end of file diff --git a/main/io/php_io_solaris.h b/main/io/php_io_solaris.h index 873b94c9e4882..89298cf991112 100644 --- a/main/io/php_io_solaris.h +++ b/main/io/php_io_solaris.h @@ -16,8 +16,7 @@ #define PHP_IO_SOLARIS_H /* Copy operations */ -zend_result php_io_solaris_copy_file_to_generic( - int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ @@ -30,4 +29,4 @@ zend_result php_io_solaris_copy_file_to_generic( #define PHP_IO_PLATFORM_NAME "solaris" -#endif /* PHP_IO_SOLARIS_H */ +#endif /* PHP_IO_SOLARIS_H */ \ No newline at end of file diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h index 4405c6ba5d418..306e89afea4a6 100644 --- a/main/io/php_io_windows.h +++ b/main/io/php_io_windows.h @@ -16,9 +16,8 @@ #define PHP_IO_WINDOWS_H /* Copy operations */ -zend_result php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t len, size_t *copied); -zend_result php_io_windows_copy_file_to_generic( - int src_fd, int dest_fd, size_t len, size_t *copied); +ssize_t php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen); +ssize_t php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ @@ -31,4 +30,4 @@ zend_result php_io_windows_copy_file_to_generic( #define PHP_IO_PLATFORM_NAME "windows" -#endif /* PHP_IO_WINDOWS_H */ +#endif /* PHP_IO_WINDOWS_H */ \ No newline at end of file diff --git a/main/php_io.h b/main/php_io.h index d56ef1dd7c1ba..6e9d2fd2b078b 100644 --- a/main/php_io.h +++ b/main/php_io.h @@ -26,12 +26,15 @@ typedef enum { PHP_IO_FD_GENERIC, /* Socket, pipe, or other - use generic operations */ } php_io_fd_type; +/* Copy as much as possible */ +#define PHP_IO_COPY_ALL SIZE_MAX + /* Synchronous copy operations vtable */ typedef struct php_io_copy_ops { - zend_result (*file_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); - zend_result (*file_to_generic)(int src_fd, int dest_fd, size_t len, size_t *copied); - zend_result (*generic_to_file)(int src_fd, int dest_fd, size_t len, size_t *copied); - zend_result (*generic_to_generic)(int src_fd, int dest_fd, size_t len, size_t *copied); + ssize_t (*file_to_file)(int src_fd, int dest_fd, size_t maxlen); + ssize_t (*file_to_generic)(int src_fd, int dest_fd, size_t maxlen); + ssize_t (*generic_to_file)(int src_fd, int dest_fd, size_t maxlen); + ssize_t (*generic_to_generic)(int src_fd, int dest_fd, size_t maxlen); } php_io_copy_ops; /* Main php_io structure */ @@ -40,14 +43,18 @@ typedef struct php_io { const char *platform_name; } php_io; -/* IO struct accessor functions */ +/* IO struct accessor function */ PHPAPI php_io *php_io_get(void); -/* Utility functions */ -PHPAPI php_io_fd_type php_io_detect_fd_type(int fd); - -/* High-level copy function - automatically selects best method based on fd types */ -PHPAPI zend_result php_io_copy(int src_fd, php_io_fd_type src_type, int dest_fd, - php_io_fd_type dest_type, size_t len, size_t *copied); +/* High-level copy function - automatically selects best method based on fd types + * Copies up to maxlen bytes from src_fd to dest_fd + * If maxlen is PHP_IO_COPY_ALL, copies until EOF + * + * Returns: + * >= 0 - number of bytes copied (may be less than maxlen if EOF reached) + * -1 - I/O error occurred (errno is set) + */ +PHPAPI ssize_t php_io_copy( + int src_fd, php_io_fd_type src_type, int dest_fd, php_io_fd_type dest_type, size_t maxlen); #endif /* PHP_IO_H */ diff --git a/main/streams/streams.c b/main/streams/streams.c index 85d2947c28a6c..db7de4486a19b 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -24,6 +24,7 @@ #include "php_globals.h" #include "php_memory_streams.h" #include "php_network.h" +#include "php_io.h" #include "php_open_temporary_file.h" #include "ext/standard/file.h" #include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */ @@ -1636,164 +1637,17 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, bool return result; } -/* Returns SUCCESS/FAILURE and sets *len to the number of bytes moved */ -PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *dest, size_t maxlen, size_t *len STREAMS_DC) +/* Fallback copy stream function */ +static ssize_t php_stream_copy_fallback(php_stream *src, php_stream *dest, size_t maxlen, size_t *len) { char buf[CHUNK_SIZE]; size_t haveread = 0; - size_t towrite; - size_t dummy; - - if (!len) { - len = &dummy; - } - - if (maxlen == 0) { - *len = 0; - return SUCCESS; - } - -#ifdef HAVE_COPY_FILE_RANGE - if (php_stream_is(src, PHP_STREAM_IS_STDIO) && - php_stream_is(dest, PHP_STREAM_IS_STDIO) && - src->writepos == src->readpos) { - /* both php_stream instances are backed by a file descriptor, are not filtered and the - * read buffer is empty: we can use copy_file_range() */ - int src_fd, dest_fd, dest_open_flags = 0; - - /* copy_file_range does not work with O_APPEND */ - if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS && - php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0) == SUCCESS && - /* get dest open flags to check if the stream is open in append mode */ - php_stream_parse_fopen_modes(dest->mode, &dest_open_flags) == SUCCESS && - !(dest_open_flags & O_APPEND)) { - - /* clamp to INT_MAX to avoid EOVERFLOW */ - const size_t cfr_max = MIN(maxlen, (size_t)SSIZE_MAX); - - /* copy_file_range() is a Linux-specific system call which allows efficient copying - * between two file descriptors, eliminating the need to transfer data from the kernel - * to userspace and back. For networking file systems like NFS and Ceph, it even - * eliminates copying data to the client, and local filesystems like Btrfs and XFS can - * create shared extents. */ - ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, cfr_max, 0); - if (result > 0) { - size_t nbytes = (size_t)result; - haveread += nbytes; - - src->position += nbytes; - dest->position += nbytes; - - if ((maxlen != PHP_STREAM_COPY_ALL && nbytes == maxlen) || php_stream_eof(src)) { - /* the whole request was satisfied or end-of-file reached - done */ - *len = haveread; - return SUCCESS; - } - - /* there may be more data; continue copying using the fallback code below */ - } else if (result == 0) { - /* end of file */ - *len = haveread; - return SUCCESS; - } else if (result < 0) { - switch (errno) { - case EINVAL: - /* some formal error, e.g. overlapping file ranges */ - break; - - case EXDEV: - /* pre Linux 5.3 error */ - break; - - case ENOSYS: - /* not implemented by this Linux kernel */ - break; - - case EIO: - /* Some filesystems will cause failures if the max length is greater than the file length - * in certain circumstances and configuration. In those cases the errno is EIO and we will - * fall back to other methods. We cannot use stat to determine the file length upfront because - * that is prone to races and outdated caching. */ - break; - - default: - /* unexpected I/O error - give up, no fallback */ - *len = haveread; - return FAILURE; - } - - /* fall back to classic copying */ - } - } - } -#endif // HAVE_COPY_FILE_RANGE if (maxlen == PHP_STREAM_COPY_ALL) { maxlen = 0; } - if (php_stream_mmap_possible(src)) { - char *p; - - do { - /* We must not modify maxlen here, because otherwise the file copy fallback below can fail */ - size_t chunk_size, must_read, mapped; - if (maxlen == 0) { - /* Unlimited read */ - must_read = chunk_size = PHP_STREAM_MMAP_MAX; - } else { - must_read = maxlen - haveread; - if (must_read >= PHP_STREAM_MMAP_MAX) { - chunk_size = PHP_STREAM_MMAP_MAX; - } else { - /* In case the length we still have to read from the file could be smaller than the file size, - * chunk_size must not get bigger the size we're trying to read. */ - chunk_size = must_read; - } - } - - p = php_stream_mmap_range(src, php_stream_tell(src), chunk_size, PHP_STREAM_MAP_MODE_SHARED_READONLY, &mapped); - - if (p) { - ssize_t didwrite; - - if (php_stream_seek(src, mapped, SEEK_CUR) != 0) { - php_stream_mmap_unmap(src); - break; - } - - didwrite = php_stream_write(dest, p, mapped); - if (didwrite < 0) { - *len = haveread; - php_stream_mmap_unmap(src); - return FAILURE; - } - - php_stream_mmap_unmap(src); - - *len = haveread += didwrite; - - /* we've got at least 1 byte to read - * less than 1 is an error - * AND read bytes match written */ - if (mapped == 0 || mapped != didwrite) { - return FAILURE; - } - if (mapped < chunk_size) { - return SUCCESS; - } - /* If we're not reading as much as possible, so a bounded read */ - if (maxlen != 0) { - must_read -= mapped; - if (must_read == 0) { - return SUCCESS; - } - } - } - } while (p); - } - - while(1) { + while (1) { size_t readchunk = sizeof(buf); ssize_t didread; char *writeptr; @@ -1808,7 +1662,7 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de return didread < 0 ? FAILURE : SUCCESS; } - towrite = didread; + size_t towrite = didread; writeptr = buf; haveread += didread; @@ -1832,6 +1686,57 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de return SUCCESS; } +/* Returns SUCCESS/FAILURE and sets *len to the number of bytes moved */ +PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *dest, size_t maxlen, size_t *len STREAMS_DC) +{ + size_t haveread = 0; + size_t dummy; + + if (!len) { + len = &dummy; + } + + if (maxlen == 0) { + *len = 0; + return SUCCESS; + } + + /* Try to use optimized I/O if both streams are castable to fd and not filtered */ + if (src->writepos == src->readpos) { /* Read buffer must be empty */ + int src_fd, dest_fd; + + if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS && + php_stream_cast(dest, PHP_STREAM_AS_FD, (void*)&dest_fd, 0) == SUCCESS) { + + /* Determine fd types based on stream type */ + php_io_fd_type src_type = php_stream_is(src, PHP_STREAM_IS_STDIO) ? + PHP_IO_FD_FILE : PHP_IO_FD_GENERIC; + php_io_fd_type dest_type = php_stream_is(dest, PHP_STREAM_IS_STDIO) ? + PHP_IO_FD_FILE : PHP_IO_FD_GENERIC; + + /* Try optimized copy */ + size_t io_maxlen = (maxlen == PHP_STREAM_COPY_ALL) ? PHP_IO_COPY_ALL : maxlen; + ssize_t result = php_io_copy(src_fd, src_type, dest_fd, dest_type, io_maxlen); + + if (result >= 0) { + /* Success - update positions */ + haveread = result; + src->position += result; + dest->position += result; + *len = haveread; + return SUCCESS; + } + + /* I/O error occurred */ + *len = 0; + return FAILURE; + } + } + + /* Classic read/write loop fallback (if cast failed) */ + return php_stream_copy_fallback(src, dest, maxlen, len); +} + /* Returns the number of bytes moved. * Returns 1 when source len is 0. * Deprecated in favor of php_stream_copy_to_stream_ex() */ diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 403f0aa6efbfe..1f4c4a0ecefbf 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,6 +298,9 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) +ADD_SOURCES("main/streams", "php_io.c php_io_copy_windows.c"); +ADD_FLAG("CFLAGS_BD_MAIN_IO", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ADD_SOURCES("main/streams", "streams.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); From 7596d78208383ea7bf23506ffceaac9933ba57f5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 16:52:29 +0100 Subject: [PATCH 05/10] io: fix file copy when dest open in append mode --- main/streams/streams.c | 45 +++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index db7de4486a19b..f13325844f8a5 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1713,23 +1713,36 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de PHP_IO_FD_FILE : PHP_IO_FD_GENERIC; php_io_fd_type dest_type = php_stream_is(dest, PHP_STREAM_IS_STDIO) ? PHP_IO_FD_FILE : PHP_IO_FD_GENERIC; - - /* Try optimized copy */ - size_t io_maxlen = (maxlen == PHP_STREAM_COPY_ALL) ? PHP_IO_COPY_ALL : maxlen; - ssize_t result = php_io_copy(src_fd, src_type, dest_fd, dest_type, io_maxlen); - - if (result >= 0) { - /* Success - update positions */ - haveread = result; - src->position += result; - dest->position += result; - *len = haveread; - return SUCCESS; + + /* Check if destination file is opened in append mode as copy_file_range does not respect O_APPEND */ + zend_bool can_use_optimized = 1; + + if (dest_type == PHP_IO_FD_FILE && src_type == PHP_IO_FD_FILE) { + int dest_flags = 0; + if (php_stream_parse_fopen_modes(dest->mode, &dest_flags) == SUCCESS && (dest_flags & O_APPEND)) { + /* Append mode with file destination and source - cannot use optimized copy */ + can_use_optimized = 0; + } + } + + if (can_use_optimized) { + /* Try optimized copy */ + size_t io_maxlen = (maxlen == PHP_STREAM_COPY_ALL) ? PHP_IO_COPY_ALL : maxlen; + ssize_t result = php_io_copy(src_fd, src_type, dest_fd, dest_type, io_maxlen); + + if (result >= 0) { + /* Success - update positions */ + haveread = result; + src->position += result; + dest->position += result; + *len = haveread; + return SUCCESS; + } + + /* I/O error occurred */ + *len = 0; + return FAILURE; } - - /* I/O error occurred */ - *len = 0; - return FAILURE; } } From 95844cfa0718f8c8b0bac27f86ffddfa18f7de69 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 17:32:37 +0100 Subject: [PATCH 06/10] io: skip io copying for userspace streams --- main/streams/streams.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main/streams/streams.c b/main/streams/streams.c index f13325844f8a5..90b0b591afb3c 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1702,7 +1702,8 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de } /* Try to use optimized I/O if both streams are castable to fd and not filtered */ - if (src->writepos == src->readpos) { /* Read buffer must be empty */ + if (!php_stream_is(src, PHP_STREAM_IS_USERSPACE) && !php_stream_is(dest, PHP_STREAM_IS_USERSPACE) && + src->writepos == src->readpos) { /* Read buffer must be empty */ int src_fd, dest_fd; if (php_stream_cast(src, PHP_STREAM_AS_FD, (void*)&src_fd, 0) == SUCCESS && From e1844077768b32e10b1dbc9c6f1c476786565ea3 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 18:06:03 +0100 Subject: [PATCH 07/10] io: use libc copy_file_range instead of syscall --- main/io/php_io_copy_linux.c | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index fd239b1498b4b..1a1bd49e81af9 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -31,15 +31,6 @@ #include #endif -/* copy_file_range wrapper for older systems */ -#ifdef HAVE_COPY_FILE_RANGE -static ssize_t copy_file_range_wrapper( - int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) -{ - return syscall(SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); -} -#endif - ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) { #ifdef HAVE_COPY_FILE_RANGE @@ -49,7 +40,7 @@ ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) while (remaining > 0) { /* Clamp to SSIZE_MAX to avoid issues */ size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; - ssize_t result = copy_file_range_wrapper(src_fd, NULL, dest_fd, NULL, to_copy, 0); + ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, to_copy, 0); if (result > 0) { total_copied += result; From 8366fc4d69dd37de61323dbecb095d9acaa12e79 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:16:50 +0100 Subject: [PATCH 08/10] io: fix copying of large files --- main/io/php_io_copy_bsd.c | 31 ++++++++---- main/io/php_io_copy_linux.c | 95 ++++++++++++++++++++++------------- main/io/php_io_copy_macos.c | 32 ++++++++---- main/io/php_io_copy_solaris.c | 30 +++++++---- 4 files changed, 122 insertions(+), 66 deletions(-) diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c index 89c1f80ffa51e..3bac6bcdb662a 100644 --- a/main/io/php_io_copy_bsd.c +++ b/main/io/php_io_copy_bsd.c @@ -29,23 +29,33 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) /* BSD sendfile signature: sendfile(fd, s, offset, nbytes, hdtr, sbytes, flags) */ size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + off_t src_offset = 0; + + /* Get current source file position */ + src_offset = lseek(src_fd, 0, SEEK_CUR); + + if (src_offset == (off_t) -1) { + /* Can't get position, fall back to generic copy */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } while (remaining > 0) { off_t to_send = (remaining < OFF_MAX) ? (off_t) remaining : OFF_MAX; off_t sbytes = 0; - int result = sendfile(src_fd, dest_fd, 0, to_send, NULL, &sbytes, 0); + int result = sendfile(src_fd, dest_fd, src_offset, to_send, NULL, &sbytes, 0); if (result == 0 || sbytes > 0) { /* Success or partial send */ total_copied += sbytes; + src_offset += sbytes; + if (maxlen != PHP_IO_COPY_ALL) { remaining -= sbytes; } - /* If result == 0, entire amount was sent, continue if needed */ - /* If result == -1 but sbytes > 0, partial send occurred */ - if (result == -1 || sbytes < to_send) { - return (ssize_t) total_copied; + /* If result != 0, error occurred but some data was transferred */ + if (result != 0) { + break; } } else { /* Error occurred with no data transferred */ @@ -55,14 +65,15 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) case EINVAL: case ENOTCONN: /* Various errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; default: /* Other errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } break; } @@ -71,7 +82,7 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index 1a1bd49e81af9..c80da8e5cf630 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -18,9 +18,16 @@ #include #include -/* Linux-specific includes */ -#ifdef HAVE_COPY_FILE_RANGE #include + +/* Provide copy_file_range wrapper if libc doesn't have it but kernel does */ +#if !defined(HAVE_COPY_FILE_RANGE) && defined(__NR_copy_file_range) +#define HAVE_COPY_FILE_RANGE 1 +static inline ssize_t copy_file_range( + int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) +{ + return syscall(__NR_copy_file_range, fd_in, off_in, fd_out, off_out, len, flags); +} #endif #ifdef HAVE_SENDFILE @@ -36,25 +43,33 @@ ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) #ifdef HAVE_COPY_FILE_RANGE size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + off_t src_offset = 0; + off_t dest_offset = 0; + + /* Get current file positions */ + src_offset = lseek(src_fd, 0, SEEK_CUR); + dest_offset = lseek(dest_fd, 0, SEEK_CUR); + + if (src_offset == (off_t) -1 || dest_offset == (off_t) -1) { + /* Can't get positions, fall back to generic copy */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } while (remaining > 0) { /* Clamp to SSIZE_MAX to avoid issues */ size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; - ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, to_copy, 0); + ssize_t result = copy_file_range(src_fd, &src_offset, dest_fd, &dest_offset, to_copy, 0); if (result > 0) { total_copied += result; + /* Offsets are automatically updated by copy_file_range */ + if (maxlen != PHP_IO_COPY_ALL) { remaining -= result; } - - /* If we got less than requested, we likely hit EOF */ - if ((size_t) result < to_copy) { - return (ssize_t) total_copied; - } } else if (result == 0) { - /* EOF */ - return (ssize_t) total_copied; + /* EOF - done */ + break; } else { /* Error occurred */ switch (errno) { @@ -62,9 +77,11 @@ ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) case EXDEV: case ENOSYS: /* Expected failures - fall back to generic copy */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + /* Haven't copied anything yet, can safely fall back */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* If we already copied some, return what we have */ break; default: /* Unexpected error */ @@ -75,7 +92,7 @@ ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } @@ -93,25 +110,31 @@ ssize_t php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen #ifdef HAVE_SENDFILE size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + off_t src_offset = 0; + + /* Get current source file position */ + src_offset = lseek(src_fd, 0, SEEK_CUR); + + if (src_offset == (off_t) -1) { + /* Can't get position, fall back to generic copy */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } while (remaining > 0) { /* Clamp to SSIZE_MAX */ size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; - ssize_t result = sendfile(dest_fd, src_fd, NULL, to_send); + ssize_t result = sendfile(dest_fd, src_fd, &src_offset, to_send); if (result > 0) { total_copied += result; + /* src_offset is automatically updated by sendfile */ + if (maxlen != PHP_IO_COPY_ALL) { remaining -= result; } - - /* If we got less than requested, we likely hit EOF or would block */ - if ((size_t) result < to_send) { - return (ssize_t) total_copied; - } } else if (result == 0) { - /* EOF */ - return (ssize_t) total_copied; + /* EOF - done */ + break; } else { /* Error occurred */ if (errno == EAGAIN) { @@ -119,15 +142,16 @@ ssize_t php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen return total_copied > 0 ? (ssize_t) total_copied : -1; } /* Other errors - fall back if we haven't copied anything yet */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; } /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } @@ -146,6 +170,7 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + /* splice doesn't take offsets - it uses fd's current position */ while (remaining > 0) { /* Clamp to SSIZE_MAX */ size_t to_splice = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; @@ -154,17 +179,13 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) if (result > 0) { total_copied += result; + if (maxlen != PHP_IO_COPY_ALL) { remaining -= result; } - - /* If we got less than requested, we likely hit EOF or would block */ - if ((size_t) result < to_splice) { - return (ssize_t) total_copied; - } } else if (result == 0) { - /* EOF */ - return (ssize_t) total_copied; + /* EOF - done */ + break; } else { /* Error occurred */ switch (errno) { @@ -173,18 +194,20 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) return total_copied > 0 ? (ssize_t) total_copied : -1; case EINVAL: /* splice not supported for these fds */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; case EPIPE: /* Broken pipe */ return total_copied > 0 ? (ssize_t) total_copied : -1; default: /* Other errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; } break; @@ -192,7 +215,7 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c index 973b3e808de6a..169d3ff2d3472 100644 --- a/main/io/php_io_copy_macos.c +++ b/main/io/php_io_copy_macos.c @@ -34,41 +34,51 @@ ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen /* Note: len is passed by reference and updated with bytes sent */ size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + off_t src_offset = 0; + + /* Get current source file position */ + src_offset = lseek(src_fd, 0, SEEK_CUR); + + if (src_offset == (off_t) -1) { + /* Can't get position, fall back to generic copy */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } while (remaining > 0) { off_t to_send = (remaining < OFF_MAX) ? (off_t) remaining : OFF_MAX; off_t len_sent = to_send; - int result = sendfile(src_fd, dest_fd, 0, &len_sent, NULL, 0); + int result = sendfile(src_fd, dest_fd, src_offset, &len_sent, NULL, 0); if (result == 0 || len_sent > 0) { /* Success or partial send */ total_copied += len_sent; + src_offset += len_sent; + if (maxlen != PHP_IO_COPY_ALL) { remaining -= len_sent; } - /* If we got less than requested or result != 0, stop */ - if (len_sent < to_send || result != 0) { - return (ssize_t) total_copied; + /* If result != 0, error occurred but some data was transferred */ + if (result != 0) { + break; } } else { /* Error occurred */ switch (errno) { case EAGAIN: - /* Would block */ - return total_copied > 0 ? (ssize_t) total_copied : -1; case EINVAL: case ENOTCONN: case EPIPE: /* Various errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; default: /* Other errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } break; } @@ -77,7 +87,7 @@ ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c index c1c2232d74ecc..4464581337796 100644 --- a/main/io/php_io_copy_solaris.c +++ b/main/io/php_io_copy_solaris.c @@ -28,6 +28,15 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl /* Solaris sendfilev - very powerful but complex API */ size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + off_t src_offset = 0; + + /* Get current source file position */ + src_offset = lseek(src_fd, 0, SEEK_CUR); + + if (src_offset == (off_t) -1) { + /* Can't get position, fall back to generic copy */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } while (remaining > 0) { struct sendfilevec sfv; @@ -37,7 +46,7 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl /* Set up the sendfile vector */ sfv.sfv_fd = src_fd; sfv.sfv_flag = SFV_FD; - sfv.sfv_off = 0; + sfv.sfv_off = src_offset; sfv.sfv_len = to_send; /* Perform the sendfile operation */ @@ -46,13 +55,15 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl if (result == 0 || xferred > 0) { /* Success or partial transfer */ total_copied += xferred; + src_offset += xferred; + if (maxlen != PHP_IO_COPY_ALL) { remaining -= xferred; } - /* If we got less than requested or error occurred, stop */ - if (result != 0 || xferred < to_send) { - return (ssize_t) total_copied; + /* If result != 0, error occurred but some data was transferred */ + if (result != 0) { + break; } } else { /* Error occurred with no data transferred */ @@ -63,14 +74,15 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl case EPIPE: case EAFNOSUPPORT: /* Various errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + /* Already copied some, return what we have */ break; default: /* Other errors */ - if (total_copied > 0) { - return (ssize_t) total_copied; + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } break; } @@ -79,7 +91,7 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - return (ssize_t) total_copied; + break; } } From 0339eb0a000c57faeb82757485615587e71bbdec Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:31:50 +0100 Subject: [PATCH 09/10] io: add missing header new lines --- main/io/php_io_bsd.h | 2 +- main/io/php_io_generic.h | 2 +- main/io/php_io_linux.h | 2 +- main/io/php_io_macos.h | 2 +- main/io/php_io_solaris.h | 2 +- main/io/php_io_windows.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/main/io/php_io_bsd.h b/main/io/php_io_bsd.h index 352fa801e9a51..e22cb26b50d23 100644 --- a/main/io/php_io_bsd.h +++ b/main/io/php_io_bsd.h @@ -29,4 +29,4 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen); #define PHP_IO_PLATFORM_NAME "bsd" -#endif /* PHP_IO_BSD_H */ \ No newline at end of file +#endif /* PHP_IO_BSD_H */ diff --git a/main/io/php_io_generic.h b/main/io/php_io_generic.h index 1d512473f3328..f33995dcae7ff 100644 --- a/main/io/php_io_generic.h +++ b/main/io/php_io_generic.h @@ -26,4 +26,4 @@ #define PHP_IO_PLATFORM_NAME "generic" -#endif /* PHP_IO_GENERIC_H */ \ No newline at end of file +#endif /* PHP_IO_GENERIC_H */ diff --git a/main/io/php_io_linux.h b/main/io/php_io_linux.h index e4b5ade8f11f8..962ab2e88294b 100644 --- a/main/io/php_io_linux.h +++ b/main/io/php_io_linux.h @@ -31,4 +31,4 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) #define PHP_IO_PLATFORM_NAME "linux" -#endif /* PHP_IO_LINUX_H */ \ No newline at end of file +#endif /* PHP_IO_LINUX_H */ diff --git a/main/io/php_io_macos.h b/main/io/php_io_macos.h index 7ef42f9fa2d2e..6e11ba5d0daa5 100644 --- a/main/io/php_io_macos.h +++ b/main/io/php_io_macos.h @@ -29,4 +29,4 @@ ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen #define PHP_IO_PLATFORM_NAME "macos" -#endif /* PHP_IO_MACOS_H */ \ No newline at end of file +#endif /* PHP_IO_MACOS_H */ diff --git a/main/io/php_io_solaris.h b/main/io/php_io_solaris.h index 89298cf991112..ce3f1842cc6eb 100644 --- a/main/io/php_io_solaris.h +++ b/main/io/php_io_solaris.h @@ -29,4 +29,4 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl #define PHP_IO_PLATFORM_NAME "solaris" -#endif /* PHP_IO_SOLARIS_H */ \ No newline at end of file +#endif /* PHP_IO_SOLARIS_H */ diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h index 306e89afea4a6..781e65e1ea564 100644 --- a/main/io/php_io_windows.h +++ b/main/io/php_io_windows.h @@ -30,4 +30,4 @@ ssize_t php_io_windows_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl #define PHP_IO_PLATFORM_NAME "windows" -#endif /* PHP_IO_WINDOWS_H */ \ No newline at end of file +#endif /* PHP_IO_WINDOWS_H */ From 9437053a771988c25a212f66473c5a5039b0e1f7 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:58:34 +0100 Subject: [PATCH 10/10] io: try to use loff_t for 32bit in copy_file_range --- main/io/php_io_copy_linux.c | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index c80da8e5cf630..0184632339822 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -43,18 +43,21 @@ ssize_t php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) #ifdef HAVE_COPY_FILE_RANGE size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - off_t src_offset = 0; - off_t dest_offset = 0; + loff_t src_offset = 0; + loff_t dest_offset = 0; /* Get current file positions */ - src_offset = lseek(src_fd, 0, SEEK_CUR); - dest_offset = lseek(dest_fd, 0, SEEK_CUR); + off_t current_src = lseek(src_fd, 0, SEEK_CUR); + off_t current_dest = lseek(dest_fd, 0, SEEK_CUR); - if (src_offset == (off_t) -1 || dest_offset == (off_t) -1) { + if (current_src == (off_t) -1 || current_dest == (off_t) -1) { /* Can't get positions, fall back to generic copy */ return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } + src_offset = current_src; + dest_offset = current_dest; + while (remaining > 0) { /* Clamp to SSIZE_MAX to avoid issues */ size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX;