diff --git a/configure.ac b/configure.ac index 77fc8c89cdf40..24d7d981c178f 100644 --- a/configure.ac +++ b/configure.ac @@ -580,10 +580,13 @@ AC_CHECK_FUNCS(m4_normalize([ putenv reallocarray scandir + sendfile + sendfilev setenv setitimer shutdown sigprocmask + splice statfs statvfs std_syslog @@ -1688,6 +1691,15 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/io], m4_normalize([ + php_io.c + php_io_copy_bsd.c + php_io_copy_linux.c + php_io_copy_macos.c + php_io_copy_solaris.c + ]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c diff --git a/main/io/php_io.c b/main/io/php_io.c new file mode 100644 index 0000000000000..675a3b858eee7 --- /dev/null +++ b/main/io/php_io.c @@ -0,0 +1,98 @@ +/* + +----------------------------------------------------------------------+ + | 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 + +#ifdef PHP_WIN32 +#include +#include +#else +#include +#endif + +/* Global instance - initialized at compile time */ +static php_io php_io_instance = { + .copy = PHP_IO_PLATFORM_COPY_OPS, + .platform_name = PHP_IO_PLATFORM_NAME, +}; + +/* Get global instance */ +PHPAPI php_io *php_io_get(void) +{ + return &php_io_instance; +} + +/* High-level copy function with dispatch */ +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, 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, 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, maxlen); + } else { + /* generic to generic */ + return io->copy.generic_to_generic(src_fd, dest_fd, maxlen); + } +} + +/* Generic read/write fallback implementation */ +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 = (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(src_fd, buf, to_read); + + 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) { + /* 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; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_written; + } + + if (bytes_written != bytes_read) { + /* Partial write - stop here */ + return (ssize_t) total_copied; + } + } + + return (ssize_t) total_copied; +} diff --git a/main/io/php_io_bsd.h b/main/io/php_io_bsd.h new file mode 100644 index 0000000000000..e22cb26b50d23 --- /dev/null +++ b/main/io/php_io_bsd.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_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 new file mode 100644 index 0000000000000..3bac6bcdb662a --- /dev/null +++ b/main/io/php_io_copy_bsd.c @@ -0,0 +1,98 @@ +/* + +----------------------------------------------------------------------+ + | 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 = 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, 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, error occurred but some data was transferred */ + if (result != 0) { + break; + } + } else { + /* Error occurred with no data transferred */ + switch (errno) { + case EAGAIN: + case EBUSY: + case EINVAL: + case ENOTCONN: + /* Various errors */ + 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 php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + break; + } + break; + } + + /* 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_SENDFILE */ + + /* Fallback to generic implementation */ + 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 new file mode 100644 index 0000000000000..0184632339822 --- /dev/null +++ b/main/io/php_io_copy_linux.c @@ -0,0 +1,234 @@ +/* + +----------------------------------------------------------------------+ + | 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 + +#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 +#include +#endif + +#ifdef HAVE_SPLICE +#include +#endif + +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); + + if (result > 0) { + total_copied += result; + /* Offsets are automatically updated by copy_file_range */ + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= result; + } + } else if (result == 0) { + /* EOF - done */ + break; + } else { + /* Error occurred */ + switch (errno) { + case EINVAL: + case EXDEV: + case ENOSYS: + /* Expected failures - fall back to generic copy */ + 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 */ + 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; + } + } + + 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, maxlen); +} + +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); + + 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 - done */ + 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); + } + /* Already copied some, return what we have */ + break; + } + + /* 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 + + /* Fallback to generic read/write loop */ + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); +} + +ssize_t php_io_linux_copy_generic_to_any(int src_fd, int dest_fd, size_t maxlen) +{ +#ifdef HAVE_SPLICE + 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); + + if (result > 0) { + total_copied += result; + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= result; + } + } else if (result == 0) { + /* EOF - done */ + 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); + } + /* 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); + } + /* Already copied some, return what we have */ + break; + } + break; + } + + /* 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 */ + + /* Fallback to generic read/write loop */ + 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 new file mode 100644 index 0000000000000..169d3ff2d3472 --- /dev/null +++ b/main/io/php_io_copy_macos.c @@ -0,0 +1,103 @@ +/* + +----------------------------------------------------------------------+ + | 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 = 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, 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 result != 0, error occurred but some data was transferred */ + if (result != 0) { + break; + } + } else { + /* Error occurred */ + switch (errno) { + case EAGAIN: + case EINVAL: + case ENOTCONN: + case EPIPE: + /* Various errors */ + 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 php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + break; + } + break; + } + + /* 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_SENDFILE */ + + /* Fallback to generic implementation */ + 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 new file mode 100644 index 0000000000000..4464581337796 --- /dev/null +++ b/main/io/php_io_copy_solaris.c @@ -0,0 +1,107 @@ +/* + +----------------------------------------------------------------------+ + | 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_SENDFILEV + /* 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; + 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); + + if (result == 0 || xferred > 0) { + /* Success or partial transfer */ + total_copied += xferred; + src_offset += xferred; + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= xferred; + } + + /* If result != 0, error occurred but some data was transferred */ + if (result != 0) { + break; + } + } 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 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 php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); + } + break; + } + break; + } + + /* 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_SENDFILEV */ + + /* Fallback to generic implementation */ + 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 new file mode 100644 index 0000000000000..867ade9be6ef5 --- /dev/null +++ b/main/io/php_io_copy_windows.c @@ -0,0 +1,99 @@ +/* + +----------------------------------------------------------------------+ + | 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" + +#ifdef PHP_WIN32 + +#include +#include +#include + +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) { + char buffer[65536]; + DWORD total_copied = 0; + DWORD remaining = (maxlen == PHP_IO_COPY_ALL) ? MAXDWORD : (DWORD) min(maxlen, MAXDWORD); + + while (remaining > 0) { + DWORD to_read = min(sizeof(buffer), remaining); + DWORD bytes_read, bytes_written; + + if (!ReadFile(src_handle, buffer, to_read, &bytes_read, NULL)) { + /* Read error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + + if (bytes_read == 0) { + /* EOF */ + return (ssize_t) total_copied; + } + + if (!WriteFile(dest_handle, buffer, bytes_read, &bytes_written, NULL)) { + /* Write error */ + return total_copied > 0 ? (ssize_t) total_copied : -1; + } + + total_copied += bytes_written; + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= bytes_written; + } + + 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, 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 (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; + } + + /* 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 */ + } + } + + /* Fallback to generic implementation */ + 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 new file mode 100644 index 0000000000000..f33995dcae7ff --- /dev/null +++ b/main/io/php_io_generic.h @@ -0,0 +1,29 @@ +/* + +----------------------------------------------------------------------+ + | 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 - all use the generic fallback */ +#define PHP_IO_PLATFORM_COPY_OPS \ + { \ + .file_to_file = 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" + +#endif /* PHP_IO_GENERIC_H */ diff --git a/main/io/php_io_internal.h b/main/io/php_io_internal.h new file mode 100644 index 0000000000000..66b1dbf80e59e --- /dev/null +++ b/main/io/php_io_internal.h @@ -0,0 +1,38 @@ +/* + +----------------------------------------------------------------------+ + | 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 */ +ssize_t php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen); + +/* 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..962ab2e88294b --- /dev/null +++ b/main/io/php_io_linux.h @@ -0,0 +1,34 @@ +/* + +----------------------------------------------------------------------+ + | 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 */ +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 \ + { \ + .file_to_file = php_io_linux_copy_file_to_file, \ + .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 */ diff --git a/main/io/php_io_macos.h b/main/io/php_io_macos.h new file mode 100644 index 0000000000000..6e11ba5d0daa5 --- /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 */ +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 \ + { \ + .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" + +#endif /* PHP_IO_MACOS_H */ diff --git a/main/io/php_io_solaris.h b/main/io/php_io_solaris.h new file mode 100644 index 0000000000000..ce3f1842cc6eb --- /dev/null +++ b/main/io/php_io_solaris.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_SOLARIS_H +#define PHP_IO_SOLARIS_H + +/* Copy operations */ +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 \ + { \ + .file_to_file = 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" + +#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..781e65e1ea564 --- /dev/null +++ b/main/io/php_io_windows.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_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); + +/* 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, \ + } + +#define PHP_IO_PLATFORM_NAME "windows" + +#endif /* PHP_IO_WINDOWS_H */ diff --git a/main/php_io.h b/main/php_io.h new file mode 100644 index 0000000000000..6e9d2fd2b078b --- /dev/null +++ b/main/php_io.h @@ -0,0 +1,60 @@ +/* + +----------------------------------------------------------------------+ + | 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; + +/* File descriptor types */ +typedef enum { + 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; + +/* Copy as much as possible */ +#define PHP_IO_COPY_ALL SIZE_MAX + +/* Synchronous copy operations vtable */ +typedef struct php_io_copy_ops { + 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 */ +typedef struct php_io { + php_io_copy_ops copy; + const char *platform_name; +} php_io; + +/* IO struct accessor function */ +PHPAPI php_io *php_io_get(void); + +/* 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..90b0b591afb3c 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,71 @@ 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 (!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 && + 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; + + /* 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; + } + } + } + + /* Classic read/write loop fallback (if cast failed) */ + return php_stream_copy_fallback(src, dest, maxlen, len); +} + /* Returns the number of bytes moved. * Returns 1 when source len is 0. * Deprecated in favor of php_stream_copy_to_stream_ex() */ diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 403f0aa6efbfe..1f4c4a0ecefbf 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,6 +298,9 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) +ADD_SOURCES("main/streams", "php_io.c php_io_copy_windows.c"); +ADD_FLAG("CFLAGS_BD_MAIN_IO", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ADD_SOURCES("main/streams", "streams.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1");