diff --git a/configure.ac b/configure.ac index 07d64902ebf0b..15fc7e4951649 100644 --- a/configure.ac +++ b/configure.ac @@ -580,10 +580,12 @@ AC_CHECK_FUNCS(m4_normalize([ putenv reallocarray scandir + sendfile setenv setitimer shutdown sigprocmask + splice statfs statvfs std_syslog @@ -1688,6 +1690,12 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/io], m4_normalize([ + php_io.c + php_io_copy_linux.c + ]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c 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/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/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); 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_copy_linux.c b/main/io/php_io_copy_linux.c new file mode 100644 index 0000000000000..0c67270d8ab09 --- /dev/null +++ b/main/io/php_io_copy_linux.c @@ -0,0 +1,267 @@ +/* + +----------------------------------------------------------------------+ + | 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; + + 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); + + if (result > 0) { + total_copied += result; + /* File positions 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; + + 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); + + 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 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; + } + } + + /* 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 + 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; + + while (remaining > 0) { + size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; + + /* src_fd → pipe */ + ssize_t in_pipe = splice(src_fd, NULL, pipefd[1], NULL, to_copy, 0); + + if (in_pipe < 0) { + /* 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]); + + /* 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; + } + + if (in_pipe == 0) { + /* EOF */ + break; + } + + /* 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; + } + + 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; + } + 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; + } + pipe_remaining -= out; + total_copied += out; + } + + if (maxlen != PHP_IO_COPY_ALL) { + remaining -= in_pipe; + } + + if (maxlen != PHP_IO_COPY_ALL && remaining == 0) { + break; + } + } + + close(pipefd[0]); + close(pipefd[1]); + + return total_copied > 0 ? (ssize_t) total_copied : -1; +#endif + + return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen); +} + +#endif /* __linux__ */ diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c new file mode 100644 index 0000000000000..758110d954dc5 --- /dev/null +++ b/main/io/php_io_copy_windows.c @@ -0,0 +1,232 @@ +/* + +----------------------------------------------------------------------+ + | 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 + +/* Read from a socket using recv() */ +static inline ssize_t php_io_windows_socket_read(SOCKET sock, char *buf, size_t len) +{ + 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; +} + +/* 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; +} + +/* 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; +} + +/* 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; +} + +/* 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; + } + + 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; + writeptr += bytes_written; + to_write -= bytes_written; + } + + 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); +} + +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); + } + + 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) +{ + HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); + SOCKET sock = (SOCKET) dest_fd; + + if (file_handle == INVALID_HANDLE_VALUE) { + return -1; + } + + /* 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 dest is actually a socket */ + if (WSAGetLastError() == WSAENOTSOCK) { + /* Reset file position for fallback */ + SetFilePointerEx(file_handle, file_pos, NULL, FILE_BEGIN); + } + } + } + + /* 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_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..09c9c1ce9196d --- /dev/null +++ b/main/io/php_io_internal.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_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" +#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..b6a0a8808a35d --- /dev/null +++ b/main/io/php_io_windows.h @@ -0,0 +1,21 @@ +#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_windows_copy_generic_to_file, \ + .generic_to_generic = php_io_windows_copy_generic_to_generic, \ + } + +#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 aefcfb5f82474..fa75404748b0d 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/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 \ 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"); @@ -309,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); 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()