From 2c23c063663f257a21c1330cd7d1a618015bb37b Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 28 Sep 2025 23:00:46 +0200 Subject: [PATCH 01/19] 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 ac9a68fabb99e45b3a23077b71e3488d89380bc3 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Nov 2025 20:29:40 +0100 Subject: [PATCH 02/19] 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 cddb4b20373a1ceba9bd872bbd397bd95579a732 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 4 Nov 2025 23:00:37 +0100 Subject: [PATCH 03/19] 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 3c85331c8b37199eb4196363cf96055035a5fbbe Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 16:28:54 +0100 Subject: [PATCH 04/19] 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 07d64902ebf0b..cae768d963902 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 aefcfb5f82474..83918e4353bb1 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 78e28bd19279dea93624c4e71f1255c6f8fe0692 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 16:52:29 +0100 Subject: [PATCH 05/19] 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 d2c846793a878ba52098c55dfad97eb8453d4819 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 17:32:37 +0100 Subject: [PATCH 06/19] 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 950caaf03bc53557c93fe4af77bc13e7b99cd068 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 18:06:03 +0100 Subject: [PATCH 07/19] 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 bfcc3e7c30c391a7e7f6ef750769dfbcb3ad3060 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:16:50 +0100 Subject: [PATCH 08/19] 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 1cb0a2082d53bdf7e8bbc3b85a8700348de40148 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:31:50 +0100 Subject: [PATCH 09/19] 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 b04885fb47ca52d4683a067538201abe3008042a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 5 Nov 2025 19:58:34 +0100 Subject: [PATCH 10/19] 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; From 280cc5c02f7a2e3566fc331261a47bca99dd2b3f Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 26 Nov 2025 12:27:49 +0100 Subject: [PATCH 11/19] io: remove broken copy_file_range offset handling --- main/io/php_io_copy_linux.c | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index 0184632339822..4cc2337a38af9 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -43,30 +43,15 @@ 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; - loff_t src_offset = 0; - loff_t dest_offset = 0; - - /* Get current file positions */ - off_t current_src = lseek(src_fd, 0, SEEK_CUR); - off_t current_dest = lseek(dest_fd, 0, SEEK_CUR); - - 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; - ssize_t result = copy_file_range(src_fd, &src_offset, dest_fd, &dest_offset, to_copy, 0); + ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, to_copy, 0); if (result > 0) { total_copied += result; - /* Offsets are automatically updated by copy_file_range */ - + /* File positions are automatically updated by copy_file_range */ if (maxlen != PHP_IO_COPY_ALL) { remaining -= result; } From 9f31164589413f82ac8709e8390c998abc152e8d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 26 Nov 2025 14:58:57 +0100 Subject: [PATCH 12/19] io: fix windows io build --- win32/build/config.w32 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 83918e4353bb1..fa75404748b0d 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,7 +298,7 @@ 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_SOURCES("main/io", "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 \ @@ -312,7 +312,7 @@ ADD_SOURCES("win32", "dllmain.c readdir.c \ ADD_FLAG("CFLAGS_BD_WIN32", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); -PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/streams/ win32/"); +PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/io main/streams/ win32/"); PHP_INSTALL_HEADERS("Zend/Optimizer", "zend_call_graph.h zend_cfg.h zend_dfg.h zend_dump.h zend_func_info.h zend_inference.h zend_optimizer.h zend_ssa.h zend_worklist.h"); STDOUT.WriteBlankLines(1); From 7560a440321c9037cd3efe22c72aba379e3547d4 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 31 Dec 2025 13:34:43 +0100 Subject: [PATCH 13/19] io: fix zend_test copy_file_range wrapper len condition --- ext/zend_test/test.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index 0faf65f36437f..f0509d94ea54f 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -1820,7 +1820,7 @@ typedef off_t off64_t; PHP_ZEND_TEST_API ssize_t copy_file_range(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags) { ssize_t (*original_copy_file_range)(int, off64_t *, int, off64_t *, size_t, unsigned int) = dlsym(RTLD_NEXT, "copy_file_range"); - if (ZT_G(limit_copy_file_range) >= Z_L(0)) { + if (ZT_G(limit_copy_file_range) >= Z_L(0) && ZT_G(limit_copy_file_range) < len) { len = ZT_G(limit_copy_file_range); } return original_copy_file_range(fd_in, off_in, fd_out, off_out, len, flags); From abf5aac2d4f52b54f2446957e60b361e7eb36874 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 31 Dec 2025 16:46:22 +0100 Subject: [PATCH 14/19] io: rewrite and fix linux splice handling and add more tests --- ...m_copy_to_stream_socket_to_file_large.phpt | 24 ++++ ..._copy_to_stream_socket_to_file_medium.phpt | 25 ++++ ...m_copy_to_stream_socket_to_file_tiny.phpt} | 2 +- ...tream_copy_to_stream_socket_to_socket.phpt | 36 +++++ main/io/php_io_copy_linux.c | 125 ++++++++++++------ 5 files changed, 173 insertions(+), 39 deletions(-) create mode 100644 ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt create mode 100644 ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_medium.phpt rename ext/standard/tests/streams/{stream_copy_to_stream_socket.phpt => stream_copy_to_stream_socket_to_file_tiny.phpt} (88%) create mode 100644 ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt new file mode 100644 index 0000000000000..c320c7cb9b229 --- /dev/null +++ b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt @@ -0,0 +1,24 @@ +--TEST-- +stream_copy_to_stream() 200k bytes with socket as $source and file as $dest +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(200000) diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_medium.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_medium.phpt new file mode 100644 index 0000000000000..b7c0b5dc7d738 --- /dev/null +++ b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_medium.phpt @@ -0,0 +1,25 @@ +--TEST-- +stream_copy_to_stream() 2048 bytes with socket as $source and file as $dest +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +string(2048) "aaaaa%saaa" + diff --git a/ext/standard/tests/streams/stream_copy_to_stream_socket.phpt b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_tiny.phpt similarity index 88% rename from ext/standard/tests/streams/stream_copy_to_stream_socket.phpt rename to ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_tiny.phpt index dafe90e40c405..2a05045576ee4 100644 --- a/ext/standard/tests/streams/stream_copy_to_stream_socket.phpt +++ b/ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_tiny.phpt @@ -1,5 +1,5 @@ --TEST-- -stream_copy_to_stream() with socket as $source +stream_copy_to_stream() single byte with socket as $source and file as $dest --SKIPIF-- +--FILE-- + +--EXPECT-- +int(10000) +int(10000) +bool(true) \ No newline at end of file diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index 4cc2337a38af9..7e659ad45a893 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -155,64 +155,113 @@ 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) { #ifdef HAVE_SPLICE + int pipefd[2]; + if (pipe(pipefd) == -1) { + return php_io_generic_copy_fallback(src_fd, dest_fd, 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; - ssize_t result - = splice(src_fd, NULL, dest_fd, NULL, to_splice, SPLICE_F_MOVE | SPLICE_F_MORE); + size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; - if (result > 0) { - total_copied += result; + /* src_fd → pipe */ + ssize_t in_pipe = splice(src_fd, NULL, pipefd[1], NULL, to_copy, 0); + + if (in_pipe < 0) { + /* Error on splice in - drain pipe if anything is there, then fall back */ + char drain_buf[1024]; + ssize_t drained; + while ((drained = read(pipefd[0], drain_buf, sizeof(drain_buf))) > 0) { + ssize_t written = write(dest_fd, drain_buf, drained); + if (written <= 0) { + close(pipefd[0]); + close(pipefd[1]); + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + total_copied += written; + } + close(pipefd[0]); + close(pipefd[1]); + /* Continue with generic fallback for remaining data */ if (maxlen != PHP_IO_COPY_ALL) { - remaining -= result; + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; } - } else if (result == 0) { - /* EOF - done */ + if (remaining > 0) { + ssize_t fallback_result = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + + if (in_pipe == 0) { + /* EOF */ break; - } else { - /* Error occurred */ - switch (errno) { - case EAGAIN: - /* Would block */ - return total_copied > 0 ? (ssize_t) total_copied : -1; - case EINVAL: - /* splice not supported for these fds */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + + /* pipe → dest_fd */ + size_t pipe_remaining = in_pipe; + while (pipe_remaining > 0) { + ssize_t out = splice(pipefd[0], NULL, dest_fd, NULL, pipe_remaining, 0); + if (out <= 0) { + /* Error on splice out - need to drain the pipe first */ + char drain_buf[1024]; + while (pipe_remaining > 0) { + size_t to_drain = (pipe_remaining < sizeof(drain_buf)) ? pipe_remaining + : sizeof(drain_buf); + ssize_t drained = read(pipefd[0], drain_buf, to_drain); + if (drained <= 0) { + break; } - /* 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 php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + + ssize_t written = write(dest_fd, drain_buf, drained); + if (written <= 0) { + close(pipefd[0]); + close(pipefd[1]); + return total_copied > 0 ? (ssize_t) total_copied : -1; } - /* Already copied some, return what we have */ - break; + pipe_remaining -= written; + total_copied += written; + } + close(pipefd[0]); + close(pipefd[1]); + + /* Continue with generic fallback for remaining data */ + if (maxlen != PHP_IO_COPY_ALL) { + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; + } + if (remaining > 0) { + ssize_t fallback_result + = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; } - break; + pipe_remaining -= out; + total_copied += out; + } + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= in_pipe; } - /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { break; } } - if (total_copied > 0) { - return (ssize_t) total_copied; - } -#endif /* HAVE_SPLICE */ + close(pipefd[0]); + close(pipefd[1]); + + return total_copied > 0 ? (ssize_t) total_copied : -1; +#endif - /* Fallback to generic read/write loop */ return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } From 3e02f08d93a336bd3330dbac14637f4f49911ca2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 31 Dec 2025 19:16:35 +0100 Subject: [PATCH 15/19] io: fix some sendfile issues and test it --- configure.ac | 1 - ..._copy_to_stream_file_to_socket_medium.phpt | 35 ++++++++++ main/io/php_io_copy_bsd.c | 49 ++++++++----- main/io/php_io_copy_linux.c | 49 +++++++------ main/io/php_io_copy_macos.c | 47 ++++++++----- main/io/php_io_copy_solaris.c | 70 ++++++++----------- 6 files changed, 157 insertions(+), 94 deletions(-) create mode 100644 ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt diff --git a/configure.ac b/configure.ac index cae768d963902..b69da7d16b252 100644 --- a/configure.ac +++ b/configure.ac @@ -581,7 +581,6 @@ AC_CHECK_FUNCS(m4_normalize([ reallocarray scandir sendfile - sendfilev setenv setitimer shutdown diff --git a/ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt b/ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt new file mode 100644 index 0000000000000..4abc2620d1509 --- /dev/null +++ b/ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt @@ -0,0 +1,35 @@ +--TEST-- +stream_copy_to_stream() 16k with file as $source and socket as $dest +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(16384) +int(16384) +bool(true) diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c index 3bac6bcdb662a..6af24274dc2ca 100644 --- a/main/io/php_io_copy_bsd.c +++ b/main/io/php_io_copy_bsd.c @@ -29,13 +29,11 @@ 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; + off_t src_offset; /* 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); } @@ -44,40 +42,58 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) off_t 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 */ + if (sbytes > 0) { + /* Some data was transferred */ total_copied += sbytes; src_offset += sbytes; if (maxlen != PHP_IO_COPY_ALL) { remaining -= sbytes; } + } - /* If result != 0, error occurred but some data was transferred */ - if (result != 0) { + if (result == 0) { + /* Success - continue */ + if (sbytes == 0) { + /* No data transferred and success = EOF */ break; } } else { - /* Error occurred with no data transferred */ + /* Error occurred */ switch (errno) { - case EAGAIN: - case EBUSY: case EINVAL: + case ENOTSOCK: case ENOTCONN: - /* Various errors */ + /* sendfile not supported - fall back */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - /* Already copied some, return what we have */ - break; + /* Continue with fallback for remaining data */ + if (maxlen != PHP_IO_COPY_ALL) { + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; + } + if (remaining > 0) { + /* Update file position for fallback */ + if (lseek(src_fd, src_offset, SEEK_SET) != (off_t) -1) { + ssize_t fallback_result + = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; + case EAGAIN: + case EBUSY: + /* Would block - return what we have */ + return total_copied > 0 ? (ssize_t) total_copied : -1; default: /* Other errors */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - break; + return total_copied > 0 ? (ssize_t) total_copied : -1; } - break; } /* For bounded copies, stop if we reached maxlen */ @@ -89,9 +105,8 @@ ssize_t php_io_bsd_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen) if (total_copied > 0) { return (ssize_t) total_copied; } -#endif /* HAVE_SENDFILE */ +#endif - /* Fallback to generic implementation */ return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index 7e659ad45a893..6a2fbff2e7eb7 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -98,24 +98,14 @@ 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, &src_offset, to_send); + ssize_t result = sendfile(dest_fd, src_fd, NULL, to_send); if (result > 0) { total_copied += result; - /* src_offset is automatically updated by sendfile */ if (maxlen != PHP_IO_COPY_ALL) { remaining -= result; @@ -125,16 +115,35 @@ ssize_t php_io_linux_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen break; } 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 php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + switch (errno) { + case EINVAL: + case ENOSYS: + /* sendfile not supported - fall back */ + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + /* Already copied some, continue with fallback for the rest */ + if (maxlen != PHP_IO_COPY_ALL) { + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; + } + if (remaining > 0) { + ssize_t fallback_result + = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; + case EAGAIN: + /* Would block - return what we have */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + default: + /* Other errors */ + if (total_copied == 0) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + return total_copied > 0 ? (ssize_t) total_copied : -1; } - /* Already copied some, return what we have */ - break; } /* For bounded copies, stop if we reached maxlen */ diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c index 169d3ff2d3472..a347d9b4900db 100644 --- a/main/io/php_io_copy_macos.c +++ b/main/io/php_io_copy_macos.c @@ -34,13 +34,11 @@ 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; + off_t src_offset; /* 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); } @@ -49,40 +47,58 @@ ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen off_t len_sent = to_send; int result = sendfile(src_fd, dest_fd, src_offset, &len_sent, NULL, 0); - if (result == 0 || len_sent > 0) { - /* Success or partial send */ + if (len_sent > 0) { + /* Some data was transferred */ total_copied += len_sent; src_offset += len_sent; if (maxlen != PHP_IO_COPY_ALL) { remaining -= len_sent; } + } - /* If result != 0, error occurred but some data was transferred */ - if (result != 0) { + if (result == 0) { + /* Success - continue */ + if (len_sent == 0) { + /* No data transferred and success = EOF */ break; } } else { /* Error occurred */ switch (errno) { - case EAGAIN: case EINVAL: + case ENOTSOCK: case ENOTCONN: - case EPIPE: - /* Various errors */ + /* sendfile not supported - fall back */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - /* Already copied some, return what we have */ - break; + /* Continue with fallback for remaining data */ + if (maxlen != PHP_IO_COPY_ALL) { + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; + } + if (remaining > 0) { + /* Update file position for fallback */ + if (lseek(src_fd, src_offset, SEEK_SET) != (off_t) -1) { + ssize_t fallback_result + = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; + case EAGAIN: + case EPIPE: + /* Would block or broken pipe - return what we have */ + return total_copied > 0 ? (ssize_t) total_copied : -1; default: /* Other errors */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - break; + return total_copied > 0 ? (ssize_t) total_copied : -1; } - break; } /* For bounded copies, stop if we reached maxlen */ @@ -94,9 +110,8 @@ ssize_t php_io_macos_copy_file_to_generic(int src_fd, int dest_fd, size_t maxlen if (total_copied > 0) { return (ssize_t) total_copied; } -#endif /* HAVE_SENDFILE */ +#endif - /* Fallback to generic implementation */ return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } diff --git a/main/io/php_io_copy_solaris.c b/main/io/php_io_copy_solaris.c index 4464581337796..3b815107e5fe2 100644 --- a/main/io/php_io_copy_solaris.c +++ b/main/io/php_io_copy_solaris.c @@ -24,72 +24,63 @@ 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 */ +#ifdef HAVE_SENDFILE size_t total_copied = 0; size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - off_t src_offset = 0; + off_t src_offset; /* 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; - size_t xferred = 0; - size_t to_send = (remaining < SIZE_MAX) ? remaining : SIZE_MAX; - - /* Set up the sendfile vector */ - sfv.sfv_fd = src_fd; - sfv.sfv_flag = SFV_FD; - sfv.sfv_off = src_offset; - sfv.sfv_len = to_send; - - /* Perform the sendfile operation */ - ssize_t result = sendfilev(dest_fd, &sfv, 1, &xferred); + size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + ssize_t result = sendfile(dest_fd, src_fd, &src_offset, to_send); - if (result == 0 || xferred > 0) { - /* Success or partial transfer */ - total_copied += xferred; - src_offset += xferred; + if (result > 0) { + total_copied += result; + /* src_offset is automatically updated by sendfile */ if (maxlen != PHP_IO_COPY_ALL) { - remaining -= xferred; - } - - /* If result != 0, error occurred but some data was transferred */ - if (result != 0) { - break; + remaining -= result; } + } else if (result == 0) { + /* EOF */ + break; } else { - /* Error occurred with no data transferred */ + /* Error occurred */ switch (errno) { - case EAGAIN: case EINVAL: - case ENOTCONN: - case EPIPE: + case ENOSYS: case EAFNOSUPPORT: - /* Various errors */ + /* sendfile not supported - fall back */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - /* Already copied some, return what we have */ - break; + /* Continue with fallback for remaining data */ + if (maxlen != PHP_IO_COPY_ALL) { + remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; + } + if (remaining > 0) { + ssize_t fallback_result + = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); + if (fallback_result > 0) { + total_copied += fallback_result; + } + } + return total_copied > 0 ? (ssize_t) total_copied : -1; + case EAGAIN: + return total_copied > 0 ? (ssize_t) total_copied : -1; default: - /* Other errors */ if (total_copied == 0) { return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - break; + return total_copied > 0 ? (ssize_t) total_copied : -1; } - break; } - /* For bounded copies, stop if we reached maxlen */ if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { break; } @@ -98,9 +89,8 @@ ssize_t php_io_solaris_copy_file_to_generic(int src_fd, int dest_fd, size_t maxl if (total_copied > 0) { return (ssize_t) total_copied; } -#endif /* HAVE_SENDFILEV */ +#endif - /* Fallback to generic implementation */ return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } From 78a9355d917e1f27c1e6d414cdeff7c9d2ece2aa Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 31 Dec 2025 19:26:10 +0100 Subject: [PATCH 16/19] io: add Mswsock.lib to win32 confutils libs for TransmitFile --- win32/build/confutils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/win32/build/confutils.js b/win32/build/confutils.js index e516fd410bcd5..be5d026e1b1b1 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -3445,7 +3445,7 @@ function toolset_setup_common_ldflags() function toolset_setup_common_libs() { // urlmon.lib ole32.lib oleaut32.lib uuid.lib gdi32.lib winspool.lib comdlg32.lib - DEFINE("LIBS", "kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib Pathcch.lib"); + DEFINE("LIBS", "kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib Pathcch.lib Mswsock.lib"); } function toolset_setup_build_mode() From 008372689979357f5f2b0b4b9f69b3745ab28362 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 28 Feb 2026 15:02:47 +0100 Subject: [PATCH 17/19] io: remove wrong pipe drain in splice --- main/io/php_io_copy_linux.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/main/io/php_io_copy_linux.c b/main/io/php_io_copy_linux.c index 6a2fbff2e7eb7..0c67270d8ab09 100644 --- a/main/io/php_io_copy_linux.c +++ b/main/io/php_io_copy_linux.c @@ -179,18 +179,8 @@ ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) ssize_t in_pipe = splice(src_fd, NULL, pipefd[1], NULL, to_copy, 0); if (in_pipe < 0) { - /* Error on splice in - drain pipe if anything is there, then fall back */ - char drain_buf[1024]; - ssize_t drained; - while ((drained = read(pipefd[0], drain_buf, sizeof(drain_buf))) > 0) { - ssize_t written = write(dest_fd, drain_buf, drained); - if (written <= 0) { - close(pipefd[0]); - close(pipefd[1]); - return total_copied > 0 ? (ssize_t) total_copied : -1; - } - total_copied += written; - } + /* Nothing was spliced into the pipe this iteration, so nothing to drain. + * Close pipe and fall back to generic copy for remaining data. */ close(pipefd[0]); close(pipefd[1]); From 114435de2083f21f0d36e946cf63514c2e128035 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 28 Feb 2026 18:02:01 +0100 Subject: [PATCH 18/19] io: remove sendfile use in bsd, macos and solaris --- configure.ac | 3 - main/io/php_io_bsd.h | 32 --------- main/io/php_io_copy_bsd.c | 113 -------------------------------- main/io/php_io_copy_macos.c | 118 ---------------------------------- main/io/php_io_copy_solaris.c | 97 ---------------------------- main/io/php_io_internal.h | 6 -- 6 files changed, 369 deletions(-) delete mode 100644 main/io/php_io_bsd.h delete mode 100644 main/io/php_io_copy_bsd.c delete mode 100644 main/io/php_io_copy_macos.c delete mode 100644 main/io/php_io_copy_solaris.c diff --git a/configure.ac b/configure.ac index b69da7d16b252..15fc7e4951649 100644 --- a/configure.ac +++ b/configure.ac @@ -1692,10 +1692,7 @@ PHP_ADD_SOURCES_X([main], 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]) diff --git a/main/io/php_io_bsd.h b/main/io/php_io_bsd.h deleted file mode 100644 index e22cb26b50d23..0000000000000 --- a/main/io/php_io_bsd.h +++ /dev/null @@ -1,32 +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 | - +----------------------------------------------------------------------+ -*/ - -#ifndef PHP_IO_BSD_H -#define PHP_IO_BSD_H - -/* Copy operations */ -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 \ - { \ - .file_to_file = 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" - -#endif /* PHP_IO_BSD_H */ diff --git a/main/io/php_io_copy_bsd.c b/main/io/php_io_copy_bsd.c deleted file mode 100644 index 6af24274dc2ca..0000000000000 --- a/main/io/php_io_copy_bsd.c +++ /dev/null @@ -1,113 +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 | - +----------------------------------------------------------------------+ -*/ - -#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) - -#include "php_io_internal.h" -#include -#include - -#ifdef HAVE_SENDFILE -#include -#include -#endif - -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) */ - size_t total_copied = 0; - size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - off_t src_offset; - - /* Get current source file position */ - src_offset = lseek(src_fd, 0, SEEK_CUR); - if (src_offset == (off_t) -1) { - 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, src_offset, to_send, NULL, &sbytes, 0); - - if (sbytes > 0) { - /* Some data was transferred */ - total_copied += sbytes; - src_offset += sbytes; - - if (maxlen != PHP_IO_COPY_ALL) { - remaining -= sbytes; - } - } - - if (result == 0) { - /* Success - continue */ - if (sbytes == 0) { - /* No data transferred and success = EOF */ - break; - } - } else { - /* Error occurred */ - switch (errno) { - case EINVAL: - case ENOTSOCK: - case ENOTCONN: - /* sendfile not supported - fall back */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - /* Continue with fallback for remaining data */ - if (maxlen != PHP_IO_COPY_ALL) { - remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; - } - if (remaining > 0) { - /* Update file position for fallback */ - if (lseek(src_fd, src_offset, SEEK_SET) != (off_t) -1) { - ssize_t fallback_result - = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); - if (fallback_result > 0) { - total_copied += fallback_result; - } - } - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - case EAGAIN: - case EBUSY: - /* Would block - return what we have */ - return total_copied > 0 ? (ssize_t) total_copied : -1; - default: - /* Other errors */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - } - } - - /* For bounded copies, stop if we reached maxlen */ - if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - break; - } - } - - if (total_copied > 0) { - return (ssize_t) total_copied; - } -#endif - - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); -} - -#endif /* FreeBSD, OpenBSD, NetBSD */ diff --git a/main/io/php_io_copy_macos.c b/main/io/php_io_copy_macos.c deleted file mode 100644 index a347d9b4900db..0000000000000 --- a/main/io/php_io_copy_macos.c +++ /dev/null @@ -1,118 +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 | - +----------------------------------------------------------------------+ -*/ - -#ifdef __APPLE__ - -#include "php_io_internal.h" -#include -#include - -#ifdef HAVE_SENDFILE -#include -#include -#endif - -#ifdef HAVE_COPYFILE -#include -#endif - -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 */ - size_t total_copied = 0; - size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; - off_t src_offset; - - /* Get current source file position */ - src_offset = lseek(src_fd, 0, SEEK_CUR); - if (src_offset == (off_t) -1) { - 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, src_offset, &len_sent, NULL, 0); - - if (len_sent > 0) { - /* Some data was transferred */ - total_copied += len_sent; - src_offset += len_sent; - - if (maxlen != PHP_IO_COPY_ALL) { - remaining -= len_sent; - } - } - - if (result == 0) { - /* Success - continue */ - if (len_sent == 0) { - /* No data transferred and success = EOF */ - break; - } - } else { - /* Error occurred */ - switch (errno) { - case EINVAL: - case ENOTSOCK: - case ENOTCONN: - /* sendfile not supported - fall back */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - /* Continue with fallback for remaining data */ - if (maxlen != PHP_IO_COPY_ALL) { - remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; - } - if (remaining > 0) { - /* Update file position for fallback */ - if (lseek(src_fd, src_offset, SEEK_SET) != (off_t) -1) { - ssize_t fallback_result - = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); - if (fallback_result > 0) { - total_copied += fallback_result; - } - } - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - case EAGAIN: - case EPIPE: - /* Would block or broken pipe - return what we have */ - return total_copied > 0 ? (ssize_t) total_copied : -1; - default: - /* Other errors */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - } - } - - /* For bounded copies, stop if we reached maxlen */ - if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - break; - } - } - - if (total_copied > 0) { - return (ssize_t) total_copied; - } -#endif - - 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 deleted file mode 100644 index 3b815107e5fe2..0000000000000 --- a/main/io/php_io_copy_solaris.c +++ /dev/null @@ -1,97 +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 | - +----------------------------------------------------------------------+ -*/ - -#ifdef __sun - -#include "php_io_internal.h" -#include -#include - -#ifdef HAVE_SENDFILEV -#include -#endif - -ssize_t php_io_solaris_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; - - /* Get current source file position */ - src_offset = lseek(src_fd, 0, SEEK_CUR); - if (src_offset == (off_t) -1) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - - while (remaining > 0) { - size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; - 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; - } - } else if (result == 0) { - /* EOF */ - break; - } else { - /* Error occurred */ - switch (errno) { - case EINVAL: - case ENOSYS: - case EAFNOSUPPORT: - /* sendfile not supported - fall back */ - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - /* Continue with fallback for remaining data */ - if (maxlen != PHP_IO_COPY_ALL) { - remaining = (total_copied < maxlen) ? maxlen - total_copied : 0; - } - if (remaining > 0) { - ssize_t fallback_result - = php_io_generic_copy_fallback(src_fd, dest_fd, remaining); - if (fallback_result > 0) { - total_copied += fallback_result; - } - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - case EAGAIN: - return total_copied > 0 ? (ssize_t) total_copied : -1; - default: - if (total_copied == 0) { - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); - } - return total_copied > 0 ? (ssize_t) total_copied : -1; - } - } - - if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { - break; - } - } - - if (total_copied > 0) { - return (ssize_t) total_copied; - } -#endif - - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); -} - -#endif /* __sun */ diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h index 66b1dbf80e59e..09c9c1ce9196d 100644 --- a/main/io/php_io_internal.h +++ b/main/io/php_io_internal.h @@ -25,12 +25,6 @@ ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen); #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 From 742b7db04ca77a9ba0d265fe77c5843bbdf585e5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 28 Feb 2026 19:15:01 +0100 Subject: [PATCH 19/19] io: do not use generic fallback on win --- main/io/php_io_copy_windows.c | 229 +++++++++++++++++++++++++++------- main/io/php_io_windows.h | 20 +-- 2 files changed, 185 insertions(+), 64 deletions(-) diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c index 867ade9be6ef5..758110d954dc5 100644 --- a/main/io/php_io_copy_windows.c +++ b/main/io/php_io_copy_windows.c @@ -20,80 +20,213 @@ #include #include -ssize_t php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) +/* Read from a socket using recv() */ +static inline ssize_t php_io_windows_socket_read(SOCKET sock, char *buf, size_t len) { - /* Use ReadFile/WriteFile for file-to-file copying */ - HANDLE src_handle = (HANDLE) _get_osfhandle(src_fd); - HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); + int to_recv = (len > INT_MAX) ? INT_MAX : (int) len; + int result = recv(sock, buf, to_recv, 0); + if (result == SOCKET_ERROR) { + return -1; + } + return (ssize_t) result; +} - if (src_handle != INVALID_HANDLE_VALUE && dest_handle != INVALID_HANDLE_VALUE) { - char buffer[65536]; - DWORD total_copied = 0; - DWORD remaining = (maxlen == PHP_IO_COPY_ALL) ? MAXDWORD : (DWORD) min(maxlen, MAXDWORD); +/* Write to a socket using send() */ +static inline ssize_t php_io_windows_socket_write(SOCKET sock, const char *buf, size_t len) +{ + int to_send = (len > INT_MAX) ? INT_MAX : (int) len; + int result = send(sock, buf, to_send, 0); + if (result == SOCKET_ERROR) { + return -1; + } + return (ssize_t) result; +} - while (remaining > 0) { - DWORD to_read = min(sizeof(buffer), remaining); - DWORD bytes_read, bytes_written; +/* Read from a file HANDLE using ReadFile() */ +static inline ssize_t php_io_windows_file_read(HANDLE handle, char *buf, size_t len) +{ + DWORD to_read = (len > MAXDWORD) ? MAXDWORD : (DWORD) len; + DWORD bytes_read; + if (!ReadFile(handle, buf, to_read, &bytes_read, NULL)) { + return -1; + } + return (ssize_t) bytes_read; +} - if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { - /* Read error */ - return total_copied > 0 ? (ssize_t) total_copied : -1; - } +/* Write to a file HANDLE using WriteFile() */ +static inline ssize_t php_io_windows_file_write(HANDLE handle, const char *buf, size_t len) +{ + DWORD to_write = (len > MAXDWORD) ? MAXDWORD : (DWORD) len; + DWORD bytes_written; + if (!WriteFile(handle, buf, to_write, &bytes_written, NULL)) { + return -1; + } + return (ssize_t) bytes_written; +} - if (bytes_read == 0) { - /* EOF */ - return (ssize_t) total_copied; - } +/* Generic copy loop parameterized by read/write function pointers */ +typedef ssize_t (*php_io_windows_read_fn)(void *handle, char *buf, size_t len); +typedef ssize_t (*php_io_windows_write_fn)(void *handle, const char *buf, size_t len); + +static ssize_t php_io_windows_copy_loop( + void *src_handle, php_io_windows_read_fn read_fn, + void *dest_handle, php_io_windows_write_fn write_fn, + size_t maxlen) +{ + char buf[8192]; + size_t total_copied = 0; + size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; + + while (remaining > 0) { + size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); + ssize_t bytes_read = read_fn(src_handle, buf, to_read); + + if (bytes_read < 0) { + return total_copied > 0 ? (ssize_t) total_copied : -1; + } else if (bytes_read == 0) { + return (ssize_t) total_copied; + } - if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { - /* Write error */ + const char *writeptr = buf; + size_t to_write = (size_t) bytes_read; + + while (to_write > 0) { + ssize_t bytes_written = write_fn(dest_handle, writeptr, to_write); + if (bytes_written <= 0) { return total_copied > 0 ? (ssize_t) total_copied : -1; } - total_copied += bytes_written; - if (maxlen != PHP_IO_COPY_ALL) { - remaining -= bytes_written; - } + writeptr += bytes_written; + to_write -= bytes_written; + } - if (bytes_written != bytes_read) { - /* Partial write */ - return (ssize_t) total_copied; - } + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_read; } + } + + return (ssize_t) total_copied; +} + +/* Wrapper functions to match the generic function pointer signatures */ +static ssize_t php_io_windows_read_file(void *handle, char *buf, size_t len) +{ + return php_io_windows_file_read((HANDLE) handle, buf, len); +} - return (ssize_t) total_copied; +static ssize_t php_io_windows_write_file(void *handle, const char *buf, size_t len) +{ + return php_io_windows_file_write((HANDLE) handle, buf, len); +} + +static ssize_t php_io_windows_read_socket(void *handle, char *buf, size_t len) +{ + return php_io_windows_socket_read((SOCKET)(uintptr_t) handle, buf, len); +} + +static ssize_t php_io_windows_write_socket(void *handle, const char *buf, size_t len) +{ + return php_io_windows_socket_write((SOCKET)(uintptr_t) handle, buf, len); +} + +ssize_t php_io_windows_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen) +{ + 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) { + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); } - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + return php_io_windows_copy_loop( + (void *) src_handle, php_io_windows_read_file, + (void *) dest_handle, php_io_windows_write_file, + maxlen); } 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); 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 = (maxlen == PHP_IO_COPY_ALL) ? 0 : (DWORD) min(maxlen, MAXDWORD); + if (file_handle == INVALID_HANDLE_VALUE) { + return -1; + } - if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { - /* 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; - } + /* Try TransmitFile for zero-copy transfer first */ + if (sock != INVALID_SOCKET) { + LARGE_INTEGER file_size; + LARGE_INTEGER file_pos; + + /* Get current file position to calculate bytes available */ + file_pos.QuadPart = 0; + if (SetFilePointerEx(file_handle, file_pos, &file_pos, FILE_CURRENT)) { + DWORD bytes_to_send; + + if (maxlen == PHP_IO_COPY_ALL) { + if (GetFileSizeEx(file_handle, &file_size)) { + LONGLONG available = file_size.QuadPart - file_pos.QuadPart; + bytes_to_send = (available > MAXDWORD) ? 0 : (DWORD) available; + } else { + bytes_to_send = 0; /* Let TransmitFile send everything */ + } + } else { + bytes_to_send = (DWORD) min(maxlen, MAXDWORD); + } + + if (TransmitFile(sock, file_handle, bytes_to_send, 0, NULL, NULL, 0)) { + /* For COPY_ALL with bytes_to_send=0, we need to figure out how much was sent */ + if (bytes_to_send == 0 && maxlen == PHP_IO_COPY_ALL) { + LARGE_INTEGER new_pos; + LARGE_INTEGER zero = {0}; + if (SetFilePointerEx(file_handle, zero, &new_pos, FILE_CURRENT)) { + return (ssize_t)(new_pos.QuadPart - file_pos.QuadPart); + } + /* Can't determine size, but succeeded */ + return 0; + } + return (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 */ + /* TransmitFile failed - check if dest is actually a socket */ + if (WSAGetLastError() == WSAENOTSOCK) { + /* Reset file position for fallback */ + SetFilePointerEx(file_handle, file_pos, NULL, FILE_BEGIN); + } } } - /* Fallback to generic implementation */ - return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + /* Fallback: file read → socket send */ + return php_io_windows_copy_loop( + (void *) file_handle, php_io_windows_read_file, + (void *)(uintptr_t) sock, php_io_windows_write_socket, + maxlen); +} + +ssize_t php_io_windows_copy_generic_to_file(int src_fd, int dest_fd, size_t maxlen) +{ + HANDLE dest_handle = (HANDLE) _get_osfhandle(dest_fd); + SOCKET sock = (SOCKET) src_fd; + + if (dest_handle == INVALID_HANDLE_VALUE) { + return -1; + } + + return php_io_windows_copy_loop( + (void *)(uintptr_t) sock, php_io_windows_read_socket, + (void *) dest_handle, php_io_windows_write_file, + maxlen); +} + +ssize_t php_io_windows_copy_generic_to_generic(int src_fd, int dest_fd, size_t maxlen) +{ + SOCKET src_sock = (SOCKET) src_fd; + SOCKET dest_sock = (SOCKET) dest_fd; + + return php_io_windows_copy_loop( + (void *)(uintptr_t) src_sock, php_io_windows_read_socket, + (void *)(uintptr_t) dest_sock, php_io_windows_write_socket, + maxlen); } #endif /* PHP_WIN32 */ diff --git a/main/io/php_io_windows.h b/main/io/php_io_windows.h index 781e65e1ea564..b6a0a8808a35d 100644 --- a/main/io/php_io_windows.h +++ b/main/io/php_io_windows.h @@ -1,31 +1,19 @@ -/* - +----------------------------------------------------------------------+ - | 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 */ 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); +ssize_t php_io_windows_copy_generic_to_file(int src_fd, int dest_fd, size_t maxlen); +ssize_t php_io_windows_copy_generic_to_generic(int src_fd, int dest_fd, size_t maxlen); /* Instance initialization macros */ #define PHP_IO_PLATFORM_COPY_OPS \ { \ .file_to_file = php_io_windows_copy_file_to_file, \ .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, \ + .generic_to_file = php_io_windows_copy_generic_to_file, \ + .generic_to_generic = php_io_windows_copy_generic_to_generic, \ } #define PHP_IO_PLATFORM_NAME "windows"