Browse Source
IO copy API for stream copying (#20399)
IO copy API for stream copying (#20399)
This introduces new API for fd copying and modifies php_stream_copy_to_stream_ex to use it. The implementation is separated for various platforms and the end result have couple of implications: - sendfile is used for copying file to generic fd (e.g. sockets) on all platforms except Windows that use TransmitFile - splice is used for copying between generic fds (e.g. sockets) on Linux - copy_file_range should get used on alpine linux with directly using syscall (as musl does not seem to implement it) - copy_file_range is used in the loop so it is used multiple times for files bigger than 2GB on Linux. - file mmap for copying is removed as it allowed crashing PHP when another process modified mapped file - this was used as a fallback for file copying. Sendfile should partially replace it. - File to file copying was optimized on Windows with use of ReadFile and WriteFile. This also adds various tests including Linux unit tests. Closes GH-20399 Co-authored-by: David Carlier <devnexen@gmail.com>pull/22808/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 2476 additions and 191 deletions
-
4.github/workflows/unit-tests.yml
-
5NEWS
-
4UPGRADING.INTERNALS
-
14configure.ac
-
71ext/openssl/tests/stream_copy_to_stream_ssl_to_file.phpt
-
14ext/openssl/xp_ssl.c
-
34ext/standard/tests/streams/stream_copy_to_stream_file_to_file_append.phpt
-
38ext/standard/tests/streams/stream_copy_to_stream_file_to_file_dest_read_ahead.phpt
-
44ext/standard/tests/streams/stream_copy_to_stream_file_to_file_over_2gb.phpt
-
57ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_maxlen.phpt
-
48ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_medium.phpt
-
53ext/standard/tests/streams/stream_copy_to_stream_file_to_socket_over_eof.phpt
-
37ext/standard/tests/streams/stream_copy_to_stream_pipe_to_file.phpt
-
54ext/standard/tests/streams/stream_copy_to_stream_pipe_to_socket.phpt
-
30ext/standard/tests/streams/stream_copy_to_stream_socket.phpt
-
23ext/standard/tests/streams/stream_copy_to_stream_socket_empty.phpt
-
42ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_large.phpt
-
48ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_maxlen.phpt
-
49ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_single.phpt
-
42ext/standard/tests/streams/stream_copy_to_stream_socket_to_file_small.phpt
-
69ext/standard/tests/streams/stream_copy_to_stream_socket_to_socket.phpt
-
32ext/standard/tests/streams/stream_copy_to_stream_socket_to_stdout.phpt
-
27ext/standard/tests/streams/stream_copy_to_stream_temp_source_partially_read.phpt
-
2ext/zend_test/test.c
-
185main/io/php_io.c
-
98main/io/php_io_copy_freebsd.c
-
354main/io/php_io_copy_linux.c
-
100main/io/php_io_copy_macos.c
-
104main/io/php_io_copy_solaris.c
-
189main/io/php_io_copy_windows.c
-
27main/io/php_io_freebsd.h
-
21main/io/php_io_generic.h
-
41main/io/php_io_internal.h
-
23main/io/php_io_linux.h
-
23main/io/php_io_macos.h
-
23main/io/php_io_solaris.h
-
23main/io/php_io_windows.h
-
53main/php_io.h
-
2main/php_streams.h
-
12main/streams/cast.c
-
5main/streams/memory.c
-
22main/streams/plain_wrapper.c
-
205main/streams/streams.c
-
12main/streams/xp_socket.c
-
4tests/unit/Makefile
-
293tests/unit/main/test_io_copy.c
-
5win32/build/config.w32
-
2win32/build/confutils.js
@ -0,0 +1,71 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() from a TLS stream copies decrypted data (no fd fast-path) |
||||
|
--EXTENSIONS-- |
||||
|
openssl |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl.pem.tmp'; |
||||
|
$cacertFile = __DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl-ca.pem.tmp'; |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$serverCtx = stream_context_create(['ssl' => [ |
||||
|
'local_cert' => '%s', |
||||
|
]]); |
||||
|
$flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; |
||||
|
$server = stream_socket_server("ssl://127.0.0.1:0", $errno, $errstr, $flags, $serverCtx); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server, 5); |
||||
|
fwrite($conn, str_repeat("secret-", 1000)); |
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
$serverCode = sprintf($serverCode, $certFile); |
||||
|
|
||||
|
$peerName = 'stream_copy_ssl_peer'; |
||||
|
$clientCode = <<<'CODE' |
||||
|
$clientCtx = stream_context_create(['ssl' => [ |
||||
|
'verify_peer' => true, |
||||
|
'cafile' => '%s', |
||||
|
'peer_name' => '%s', |
||||
|
]]); |
||||
|
$client = stream_socket_client("ssl://{{ ADDR }}", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $clientCtx); |
||||
|
|
||||
|
$tmp = tmpfile(); |
||||
|
/* If the copy offloaded the raw socket fd it would write ciphertext; the |
||||
|
* decrypted plaintext proves it correctly fell back to the userspace loop. */ |
||||
|
$copied = stream_copy_to_stream($client, $tmp); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
$content = stream_get_contents($tmp); |
||||
|
var_dump(strlen($content)); |
||||
|
var_dump($content === str_repeat("secret-", 1000)); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($client); |
||||
|
CODE; |
||||
|
$clientCode = sprintf($clientCode, $cacertFile, $peerName); |
||||
|
|
||||
|
include 'CertificateGenerator.inc'; |
||||
|
$certificateGenerator = new CertificateGenerator(); |
||||
|
$certificateGenerator->saveCaCert($cacertFile); |
||||
|
$certificateGenerator->saveNewCertAsFileWithKey($peerName, $certFile); |
||||
|
|
||||
|
include 'ServerClientTestCase.inc'; |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--CLEAN-- |
||||
|
<?php |
||||
|
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl.pem.tmp'); |
||||
|
@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'stream_copy_ssl-ca.pem.tmp'); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(7000) |
||||
|
int(7000) |
||||
|
bool(true) |
||||
@ -0,0 +1,34 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() file to file with an append-mode destination |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$srcFile = __DIR__ . '/stream_copy_append_src.txt'; |
||||
|
$dstFile = __DIR__ . '/stream_copy_append_dst.txt'; |
||||
|
|
||||
|
file_put_contents($srcFile, str_repeat("b", 3000)); |
||||
|
file_put_contents($dstFile, "PREFIX-"); |
||||
|
|
||||
|
$src = fopen($srcFile, 'r'); |
||||
|
/* O_APPEND must disable the fd-level copy fast-path and still append correctly. */ |
||||
|
$dst = fopen($dstFile, 'a'); |
||||
|
|
||||
|
$copied = stream_copy_to_stream($src, $dst); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fclose($src); |
||||
|
fclose($dst); |
||||
|
|
||||
|
$result = file_get_contents($dstFile); |
||||
|
var_dump(strlen($result)); |
||||
|
var_dump($result === "PREFIX-" . str_repeat("b", 3000)); |
||||
|
?> |
||||
|
--CLEAN-- |
||||
|
<?php |
||||
|
@unlink(__DIR__ . '/stream_copy_append_src.txt'); |
||||
|
@unlink(__DIR__ . '/stream_copy_append_dst.txt'); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(3000) |
||||
|
int(3007) |
||||
|
bool(true) |
||||
@ -0,0 +1,38 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() file to file with a partially read destination |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$srcFile = __DIR__ . '/stream_copy_dest_read_ahead_src.txt'; |
||||
|
$dstFile = __DIR__ . '/stream_copy_dest_read_ahead_dst.txt'; |
||||
|
|
||||
|
file_put_contents($srcFile, str_repeat("N", 50)); |
||||
|
file_put_contents($dstFile, str_repeat("O", 3000)); |
||||
|
|
||||
|
$src = fopen($srcFile, 'r'); |
||||
|
$dst = fopen($dstFile, 'r+'); |
||||
|
/* Buffered read-ahead moves the fd offset past the stream position; the copy |
||||
|
* must land at the stream position. */ |
||||
|
fread($dst, 10); |
||||
|
|
||||
|
$copied = stream_copy_to_stream($src, $dst); |
||||
|
var_dump($copied); |
||||
|
var_dump(ftell($dst)); |
||||
|
|
||||
|
fclose($src); |
||||
|
fclose($dst); |
||||
|
|
||||
|
$result = file_get_contents($dstFile); |
||||
|
var_dump(strlen($result)); |
||||
|
var_dump($result === str_repeat("O", 10) . str_repeat("N", 50) . str_repeat("O", 2940)); |
||||
|
?> |
||||
|
--CLEAN-- |
||||
|
<?php |
||||
|
@unlink(__DIR__ . '/stream_copy_dest_read_ahead_src.txt'); |
||||
|
@unlink(__DIR__ . '/stream_copy_dest_read_ahead_dst.txt'); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(50) |
||||
|
int(60) |
||||
|
int(3000) |
||||
|
bool(true) |
||||
@ -0,0 +1,44 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() copies files larger than 2GB in full |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test'); |
||||
|
if (PHP_INT_SIZE < 8) die('skip 64-bit only'); |
||||
|
if (getenv('SKIP_SLOW_TESTS')) die('skip slow test'); |
||||
|
$dir = sys_get_temp_dir(); |
||||
|
if (disk_free_space($dir) < 4 * 1024 * 1024 * 1024) { |
||||
|
die('skip Reason: Insufficient disk space (less than 4GB)'); |
||||
|
} |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
$size = 3 * 1024 * 1024 * 1024; // exceeds the ~2GB per-call kernel copy limit |
||||
|
$src = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_src.bin"; |
||||
|
$dst = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_dst.bin"; |
||||
|
|
||||
|
// Create a sparse 3GB source so the copy loop runs without using 3GB of data. |
||||
|
$fh = fopen($src, "wb"); |
||||
|
fseek($fh, $size - 1); |
||||
|
fwrite($fh, "\0"); |
||||
|
fclose($fh); |
||||
|
|
||||
|
$in = fopen($src, "rb"); |
||||
|
$out = fopen($dst, "wb"); |
||||
|
$copied = stream_copy_to_stream($in, $out); |
||||
|
fclose($in); |
||||
|
fclose($out); |
||||
|
|
||||
|
var_dump($copied === $size); |
||||
|
var_dump(filesize($dst) === $size); |
||||
|
|
||||
|
unlink($src); |
||||
|
unlink($dst); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
bool(true) |
||||
|
bool(true) |
||||
|
--CLEAN-- |
||||
|
<?php |
||||
|
@unlink(sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_src.bin"); |
||||
|
@unlink(sys_get_temp_dir() . DIRECTORY_SEPARATOR . "stream_copy_over_2gb_dst.bin"); |
||||
|
?> |
||||
@ -0,0 +1,57 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() file to socket with a maxlength shorter than the file (bounded sendfile + source offset) |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$result = stream_get_contents($conn); |
||||
|
|
||||
|
phpt_notify(message: strlen($result)); |
||||
|
phpt_notify(message: $result === str_repeat("A", 8192) ? "match" : "mismatch"); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$src = tmpfile(); |
||||
|
fwrite($src, str_repeat("A", 8192) . str_repeat("B", 8192)); |
||||
|
rewind($src); |
||||
|
|
||||
|
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
|
||||
|
/* Only the first 8192 bytes must be sent: the bounded sendfile path has to |
||||
|
* stop at maxlen rather than streaming to EOF. */ |
||||
|
$copied = stream_copy_to_stream($src, $dest, 8192); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
/* The source position must have advanced by exactly maxlen, so the kernel |
||||
|
* offload restored the descriptor offset to the maxlen boundary. */ |
||||
|
$rest = fread($src, 8192); |
||||
|
var_dump(strlen($rest)); |
||||
|
var_dump($rest === str_repeat("B", 8192)); |
||||
|
|
||||
|
fclose($dest); |
||||
|
fclose($src); |
||||
|
|
||||
|
var_dump((int) trim(phpt_wait())); |
||||
|
var_dump(trim(phpt_wait()) === "match"); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(8192) |
||||
|
int(8192) |
||||
|
bool(true) |
||||
|
int(8192) |
||||
|
bool(true) |
||||
@ -0,0 +1,48 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() 16k with file as $source and socket as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$data = str_repeat('data', 4096); |
||||
|
$result = stream_get_contents($conn); |
||||
|
|
||||
|
phpt_notify(message: strlen($result)); |
||||
|
phpt_notify(message: $result === $data ? "match" : "mismatch"); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$src = tmpfile(); |
||||
|
$data = str_repeat('data', 4096); |
||||
|
fwrite($src, $data); |
||||
|
rewind($src); |
||||
|
|
||||
|
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
$copied = stream_copy_to_stream($src, $dest); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fclose($dest); |
||||
|
fclose($src); |
||||
|
|
||||
|
var_dump((int) trim(phpt_wait())); |
||||
|
var_dump(trim(phpt_wait()) === "match"); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(16384) |
||||
|
int(16384) |
||||
|
bool(true) |
||||
@ -0,0 +1,53 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() file to socket with a maxlength larger than the file (sendfile stops at EOF) |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$result = stream_get_contents($conn); |
||||
|
|
||||
|
phpt_notify(message: strlen($result)); |
||||
|
phpt_notify(message: $result === str_repeat("A", 4096) ? "match" : "mismatch"); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$src = tmpfile(); |
||||
|
fwrite($src, str_repeat("A", 4096)); |
||||
|
rewind($src); |
||||
|
|
||||
|
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
|
||||
|
/* maxlen exceeds the file size: sendfile must stop at EOF and report only |
||||
|
* the bytes actually available rather than blocking for the full maxlen. */ |
||||
|
$copied = stream_copy_to_stream($src, $dest, 100000); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
/* Nothing left to read once the whole file has been consumed. */ |
||||
|
var_dump(strlen(fread($src, 4096))); |
||||
|
|
||||
|
fclose($dest); |
||||
|
fclose($src); |
||||
|
|
||||
|
var_dump((int) trim(phpt_wait())); |
||||
|
var_dump(trim(phpt_wait()) === "match"); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(4096) |
||||
|
int(0) |
||||
|
int(4096) |
||||
|
bool(true) |
||||
@ -0,0 +1,37 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() with a pipe as $source and file as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$descriptors = [1 => ['pipe', 'w']]; |
||||
|
$proc = proc_open( |
||||
|
[PHP_BINARY, '-n', '-r', 'echo str_repeat("p", 5000);'], |
||||
|
$descriptors, |
||||
|
$pipes |
||||
|
); |
||||
|
var_dump(is_resource($proc)); |
||||
|
|
||||
|
$source = $pipes[1]; |
||||
|
$tmp = tmpfile(); |
||||
|
|
||||
|
$copied = stream_copy_to_stream($source, $tmp); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
$content = stream_get_contents($tmp); |
||||
|
var_dump(strlen($content)); |
||||
|
var_dump($content === str_repeat("p", 5000)); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($source); |
||||
|
proc_close($proc); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
bool(true) |
||||
|
int(5000) |
||||
|
int(5000) |
||||
|
bool(true) |
||||
@ -0,0 +1,54 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() with a pipe as $source and a socket as $dest (splice from pipe) |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$result = stream_get_contents($conn); |
||||
|
|
||||
|
phpt_notify(message: strlen($result)); |
||||
|
phpt_notify(message: $result === str_repeat("p", 5000) ? "match" : "mismatch"); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
/* A real pipe as the source exercises the splice-from-pipe path; with a |
||||
|
* socket destination it must use the corked (SPLICE_F_MORE) variant. */ |
||||
|
$descriptors = [1 => ['pipe', 'w']]; |
||||
|
$proc = proc_open( |
||||
|
[PHP_BINARY, '-n', '-r', 'echo str_repeat("p", 5000);'], |
||||
|
$descriptors, |
||||
|
$pipes |
||||
|
); |
||||
|
|
||||
|
$source = $pipes[1]; |
||||
|
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
|
||||
|
$copied = stream_copy_to_stream($source, $dest); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fclose($dest); |
||||
|
fclose($source); |
||||
|
proc_close($proc); |
||||
|
|
||||
|
var_dump((int) trim(phpt_wait())); |
||||
|
var_dump(trim(phpt_wait()) === "match"); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(5000) |
||||
|
int(5000) |
||||
|
bool(true) |
||||
@ -1,30 +0,0 @@ |
|||||
--TEST-- |
|
||||
stream_copy_to_stream() with socket as $source |
|
||||
--SKIPIF-- |
|
||||
<?php |
|
||||
$sockets = @stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); |
|
||||
if (!$sockets) die("skip stream_socket_pair"); |
|
||||
?> |
|
||||
--FILE-- |
|
||||
<?php |
|
||||
|
|
||||
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); |
|
||||
$tmp = tmpfile(); |
|
||||
|
|
||||
fwrite($sockets[0], "a"); |
|
||||
stream_socket_shutdown($sockets[0], STREAM_SHUT_WR); |
|
||||
stream_copy_to_stream($sockets[1], $tmp); |
|
||||
|
|
||||
fseek($tmp, 0, SEEK_SET); |
|
||||
var_dump(stream_get_contents($tmp)); |
|
||||
|
|
||||
stream_copy_to_stream($sockets[1], $tmp); |
|
||||
|
|
||||
fseek($tmp, 0, SEEK_SET); |
|
||||
var_dump(stream_get_contents($tmp)); |
|
||||
|
|
||||
|
|
||||
?> |
|
||||
--EXPECT-- |
|
||||
string(1) "a" |
|
||||
string(1) "a" |
|
||||
@ -0,0 +1,23 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() from a socket already at EOF returns 0, not false |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (substr(PHP_OS, 0, 3) == 'WIN') die('skip not for Windows'); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
$file = tempnam(sys_get_temp_dir(), 'sct'); |
||||
|
$dest = fopen($file, 'wb'); |
||||
|
|
||||
|
[$a, $b] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); |
||||
|
fclose($b); // peer closed without sending; $a is at EOF |
||||
|
|
||||
|
$copied = stream_copy_to_stream($a, $dest); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fclose($a); |
||||
|
fclose($dest); |
||||
|
@unlink($file); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(0) |
||||
@ -0,0 +1,42 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() 200k bytes with socket as $source and file as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
fwrite($conn, str_repeat("a", 200000)); |
||||
|
stream_socket_shutdown($conn, STREAM_SHUT_WR); |
||||
|
|
||||
|
/* Keep alive until client is done reading. */ |
||||
|
fread($conn, 1); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
$tmp = tmpfile(); |
||||
|
|
||||
|
stream_copy_to_stream($source, $tmp); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
var_dump(strlen(stream_get_contents($tmp))); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($source); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(200000) |
||||
@ -0,0 +1,48 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() socket to file with a maxlength shorter than the data |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
fwrite($conn, str_repeat("a", 10000)); |
||||
|
stream_socket_shutdown($conn, STREAM_SHUT_WR); |
||||
|
|
||||
|
/* Keep alive until client is done reading. */ |
||||
|
fread($conn, 1); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
$tmp = tmpfile(); |
||||
|
|
||||
|
/* Only 4096 of the 10000 available bytes must be copied. */ |
||||
|
$copied = stream_copy_to_stream($source, $tmp, 4096); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
$content = stream_get_contents($tmp); |
||||
|
var_dump(strlen($content)); |
||||
|
var_dump($content === str_repeat("a", 4096)); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($source); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(4096) |
||||
|
int(4096) |
||||
|
bool(true) |
||||
@ -0,0 +1,49 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() single byte with socket as $source and file as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
fwrite($conn, "a"); |
||||
|
stream_socket_shutdown($conn, STREAM_SHUT_WR); |
||||
|
|
||||
|
/* Keep alive until client is done reading. */ |
||||
|
fread($conn, 1); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
$tmp = tmpfile(); |
||||
|
|
||||
|
stream_copy_to_stream($source, $tmp); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
var_dump(stream_get_contents($tmp)); |
||||
|
|
||||
|
/* Second copy after EOF should be a no-op. */ |
||||
|
stream_copy_to_stream($source, $tmp); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
var_dump(stream_get_contents($tmp)); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($source); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
string(1) "a" |
||||
|
string(1) "a" |
||||
@ -0,0 +1,42 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() 2048 bytes with socket as $source and file as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
fwrite($conn, str_repeat("a", 2048)); |
||||
|
stream_socket_shutdown($conn, STREAM_SHUT_WR); |
||||
|
|
||||
|
/* Keep alive until client is done reading. */ |
||||
|
fread($conn, 1); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$source = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
$tmp = tmpfile(); |
||||
|
|
||||
|
stream_copy_to_stream($source, $tmp); |
||||
|
|
||||
|
fseek($tmp, 0, SEEK_SET); |
||||
|
var_dump(stream_get_contents($tmp)); |
||||
|
|
||||
|
fclose($tmp); |
||||
|
fclose($source); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECTF-- |
||||
|
string(2048) "aaaaa%saaa" |
||||
@ -0,0 +1,69 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() socket to socket (splice both directions) |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$sourceCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
/* Send address again so the client can read it via phpt_wait(). */ |
||||
|
phpt_notify(message: stream_socket_get_name($server, false)); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$data = str_repeat('test data ', 1000); |
||||
|
fwrite($conn, $data); |
||||
|
stream_socket_shutdown($conn, STREAM_SHUT_WR); |
||||
|
|
||||
|
/* Keep alive until client is done reading. */ |
||||
|
fread($conn, 1); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$destCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server); |
||||
|
$result = stream_get_contents($conn); |
||||
|
|
||||
|
phpt_notify(message: strlen($result)); |
||||
|
phpt_notify(message: $result === str_repeat('test data ', 1000) ? "match" : "mismatch"); |
||||
|
|
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$sourceAddr = trim(phpt_wait("source")); |
||||
|
$source = stream_socket_client("tcp://$sourceAddr", $errno, $errstr, 10); |
||||
|
$dest = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
|
||||
|
$copied = stream_copy_to_stream($source, $dest); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
stream_socket_shutdown($dest, STREAM_SHUT_WR); |
||||
|
fclose($source); |
||||
|
|
||||
|
var_dump((int) trim(phpt_wait("dest"))); |
||||
|
var_dump(trim(phpt_wait("dest")) === "match"); |
||||
|
|
||||
|
fclose($dest); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, [ |
||||
|
'source' => $sourceCode, |
||||
|
'dest' => $destCode, |
||||
|
]); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(10000) |
||||
|
int(10000) |
||||
|
bool(true) |
||||
@ -0,0 +1,32 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() with socket as $source and STDOUT as $dest |
||||
|
--SKIPIF-- |
||||
|
<?php |
||||
|
if (!function_exists("proc_open")) die("skip no proc_open"); |
||||
|
?> |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
$serverCode = <<<'CODE' |
||||
|
$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr); |
||||
|
phpt_notify_server_start($server); |
||||
|
|
||||
|
$conn = stream_socket_accept($server, 5); |
||||
|
fwrite($conn, "data to stdout\n"); |
||||
|
fclose($conn); |
||||
|
fclose($server); |
||||
|
CODE; |
||||
|
|
||||
|
$clientCode = <<<'CODE' |
||||
|
$fd = stream_socket_client("tcp://{{ ADDR }}", $errno, $errstr, 10); |
||||
|
|
||||
|
stream_copy_to_stream($fd, STDOUT); |
||||
|
|
||||
|
fclose($fd); |
||||
|
CODE; |
||||
|
|
||||
|
include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); |
||||
|
ServerClientTestCase::getInstance()->run($clientCode, $serverCode); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
data to stdout |
||||
@ -0,0 +1,27 @@ |
|||||
|
--TEST-- |
||||
|
stream_copy_to_stream() with a partially read file-backed php://temp source |
||||
|
--FILE-- |
||||
|
<?php |
||||
|
|
||||
|
/* Spill php://temp to its tmpfile backing so the copy source delegates to an |
||||
|
* inner stdio stream whose buffer state the fd-level fast path cannot see. */ |
||||
|
$src = fopen('php://temp/maxmemory:16', 'r+'); |
||||
|
fwrite($src, str_repeat("A", 2000) . str_repeat("B", 2000)); |
||||
|
rewind($src); |
||||
|
/* Read-ahead on the inner stream moves its fd offset past the position. */ |
||||
|
var_dump(strlen(fread($src, 2000))); |
||||
|
|
||||
|
$dst = tmpfile(); |
||||
|
$copied = stream_copy_to_stream($src, $dst); |
||||
|
var_dump($copied); |
||||
|
|
||||
|
rewind($dst); |
||||
|
var_dump(stream_get_contents($dst) === str_repeat("B", 2000)); |
||||
|
|
||||
|
fclose($src); |
||||
|
fclose($dst); |
||||
|
?> |
||||
|
--EXPECT-- |
||||
|
int(2000) |
||||
|
int(2000) |
||||
|
bool(true) |
||||
@ -0,0 +1,185 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#include "php.h" |
||||
|
#include "php_io.h" |
||||
|
#include "php_io_internal.h" |
||||
|
|
||||
|
#include <errno.h> |
||||
|
|
||||
|
#ifdef PHP_WIN32 |
||||
|
#include <io.h> |
||||
|
#include <winsock2.h> |
||||
|
#else |
||||
|
#include <unistd.h> |
||||
|
#include <poll.h> |
||||
|
#endif |
||||
|
|
||||
|
static php_io php_io_instance = { |
||||
|
.copy = PHP_IO_PLATFORM_COPY, |
||||
|
.platform_name = PHP_IO_PLATFORM_NAME, |
||||
|
}; |
||||
|
|
||||
|
PHPAPI php_io *php_io_get(void) |
||||
|
{ |
||||
|
return &php_io_instance; |
||||
|
} |
||||
|
|
||||
|
PHPAPI zend_result php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
return php_io_get()->copy(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
char buf[PHP_IO_COPY_BUFSIZE]; |
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); |
||||
|
ssize_t bytes_read; |
||||
|
do { |
||||
|
bytes_read = read(src_fd, buf, to_read); |
||||
|
} while (bytes_read < 0 && errno == EINTR); |
||||
|
|
||||
|
if (bytes_read < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} else if (bytes_read == 0) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
char *writeptr = buf; |
||||
|
size_t to_write = (size_t) bytes_read; |
||||
|
|
||||
|
while (to_write > 0) { |
||||
|
ssize_t bytes_written; |
||||
|
do { |
||||
|
bytes_written = write(dest_fd, writeptr, to_write); |
||||
|
} while (bytes_written < 0 && errno == EINTR); |
||||
|
if (bytes_written <= 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
total_copied += bytes_written; |
||||
|
writeptr += bytes_written; |
||||
|
to_write -= bytes_written; |
||||
|
} |
||||
|
if (result == FAILURE) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= bytes_read; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/* For a blocking socket source, wait until data is available (or the configured |
||||
|
* timeout elapses) before reading. Mirrors the per-platform wait_for_data |
||||
|
* helpers so the generic copy path honours stream timeouts on systems without a |
||||
|
* kernel offload (e.g. Haiku). Returns >0 ready, 0 timeout, <0 error. */ |
||||
|
#ifndef PHP_WIN32 |
||||
|
static int php_io_generic_wait_for_data(php_io_fd *fd) |
||||
|
{ |
||||
|
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) { |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
int timeout_ms = (fd->timeout.tv_sec == -1) |
||||
|
? -1 |
||||
|
: (int) (fd->timeout.tv_sec * 1000 + fd->timeout.tv_usec / 1000); |
||||
|
|
||||
|
struct pollfd pfd; |
||||
|
pfd.fd = fd->fd; |
||||
|
pfd.events = POLLIN; |
||||
|
|
||||
|
int ret; |
||||
|
do { |
||||
|
ret = poll(&pfd, 1, timeout_ms); |
||||
|
} while (ret == -1 && errno == EINTR); |
||||
|
|
||||
|
return ret; |
||||
|
} |
||||
|
#else |
||||
|
static int php_io_generic_wait_for_data(php_io_fd *fd) |
||||
|
{ |
||||
|
(void) fd; |
||||
|
return 1; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
zend_result php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
char buf[PHP_IO_COPY_BUFSIZE]; |
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
int ready = php_io_generic_wait_for_data(src); |
||||
|
if (ready == 0) { |
||||
|
/* timeout */ |
||||
|
break; |
||||
|
} else if (ready < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
size_t to_read = (remaining < sizeof(buf)) ? remaining : sizeof(buf); |
||||
|
ssize_t bytes_read; |
||||
|
do { |
||||
|
bytes_read = read(src->fd, buf, to_read); |
||||
|
} while (bytes_read < 0 && errno == EINTR); |
||||
|
|
||||
|
if (bytes_read < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} else if (bytes_read == 0) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
char *writeptr = buf; |
||||
|
size_t to_write = (size_t) bytes_read; |
||||
|
|
||||
|
while (to_write > 0) { |
||||
|
ssize_t bytes_written; |
||||
|
do { |
||||
|
bytes_written = write(dest->fd, writeptr, to_write); |
||||
|
} while (bytes_written < 0 && errno == EINTR); |
||||
|
if (bytes_written <= 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
total_copied += bytes_written; |
||||
|
writeptr += bytes_written; |
||||
|
to_write -= bytes_written; |
||||
|
} |
||||
|
if (result == FAILURE) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= bytes_read; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
} |
||||
@ -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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#if defined(__FreeBSD__) || defined(__DragonFly__) |
||||
|
|
||||
|
#include "php_io_internal.h" |
||||
|
#include <sys/types.h> |
||||
|
#include <sys/socket.h> |
||||
|
#include <sys/uio.h> |
||||
|
#include <unistd.h> |
||||
|
#include <errno.h> |
||||
|
|
||||
|
/* Hint the kernel to read ahead a few pages from the source file so the disk |
||||
|
* I/O overlaps with the network send. SF_FLAGS() is FreeBSD-specific and may be |
||||
|
* absent on DragonFly, in which case we pass plain 0 (no readahead hint). */ |
||||
|
#ifdef SF_FLAGS |
||||
|
# define PHP_IO_FREEBSD_SF_FLAGS SF_FLAGS(16, 0) |
||||
|
#else |
||||
|
# define PHP_IO_FREEBSD_SF_FLAGS 0 |
||||
|
#endif |
||||
|
|
||||
|
static zend_result php_io_freebsd_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
off_t start_offset = lseek(src_fd, 0, SEEK_CUR); |
||||
|
if (start_offset == (off_t) -1) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
off_t total_sent = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? 0 : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (maxlen == PHP_IO_COPY_ALL || remaining > 0) { |
||||
|
off_t sent_in_this_call = 0; |
||||
|
int sendfile_result = sendfile(src_fd, dest_fd, start_offset + total_sent, remaining, NULL, |
||||
|
&sent_in_this_call, PHP_IO_FREEBSD_SF_FLAGS); |
||||
|
|
||||
|
if (sent_in_this_call > 0) { |
||||
|
total_sent += sent_in_this_call; |
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= (size_t) sent_in_this_call; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (sendfile_result == 0) { |
||||
|
if (sent_in_this_call == 0 || remaining == 0) { |
||||
|
break; |
||||
|
} |
||||
|
} else { |
||||
|
if (errno == EINTR) { |
||||
|
continue; |
||||
|
} |
||||
|
if (errno == EAGAIN) { |
||||
|
if (sent_in_this_call > 0) { |
||||
|
continue; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
if (total_sent == 0) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (total_sent > 0) { |
||||
|
/* best effort: keep reporting the delivered bytes even if this fails */ |
||||
|
lseek(src_fd, start_offset + total_sent, SEEK_SET); |
||||
|
} |
||||
|
|
||||
|
*copied = (size_t) total_sent; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
zend_result php_io_freebsd_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
/* unlike linux, sendfile on freebsd works only under those conditions */ |
||||
|
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
return php_io_freebsd_sendfile(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
/* php_io_generic_copy honours the stream timeout for socket sources */ |
||||
|
return php_io_generic_copy(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#endif |
||||
@ -0,0 +1,354 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifdef __linux__ |
||||
|
|
||||
|
#include "php_io_internal.h" |
||||
|
#include <unistd.h> |
||||
|
#include <errno.h> |
||||
|
#include <sys/syscall.h> |
||||
|
|
||||
|
#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 <sys/sendfile.h> |
||||
|
#endif |
||||
|
|
||||
|
#ifdef HAVE_SPLICE |
||||
|
#include <fcntl.h> |
||||
|
#include <sys/socket.h> |
||||
|
#include <netinet/in.h> |
||||
|
#include <netinet/tcp.h> |
||||
|
#endif |
||||
|
|
||||
|
static inline int php_io_linux_wait_for_data(php_io_fd *fd) |
||||
|
{ |
||||
|
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) { |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
struct timeval *ptimeout = (fd->timeout.tv_sec == -1) ? NULL : &fd->timeout; |
||||
|
int timeout_ms; |
||||
|
|
||||
|
if (ptimeout == NULL) { |
||||
|
timeout_ms = -1; |
||||
|
} else { |
||||
|
timeout_ms = ptimeout->tv_sec * 1000 + ptimeout->tv_usec / 1000; |
||||
|
} |
||||
|
|
||||
|
struct pollfd pfd; |
||||
|
pfd.fd = fd->fd; |
||||
|
pfd.events = POLLIN; |
||||
|
|
||||
|
int ret; |
||||
|
do { |
||||
|
ret = poll(&pfd, 1, timeout_ms); |
||||
|
} while (ret == -1 && errno == EINTR); |
||||
|
|
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
static zend_result php_io_linux_copy_file_to_file(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
#ifdef HAVE_COPY_FILE_RANGE |
||||
|
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; |
||||
|
ssize_t result = copy_file_range(src_fd, NULL, dest_fd, NULL, to_copy, 0); |
||||
|
|
||||
|
if (result > 0) { |
||||
|
total_copied += result; |
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= result; |
||||
|
} |
||||
|
} else if (result == 0) { |
||||
|
break; |
||||
|
} else { |
||||
|
switch (errno) { |
||||
|
case EINVAL: |
||||
|
case EXDEV: |
||||
|
case ENOSYS: |
||||
|
case EIO: |
||||
|
if (total_copied == 0) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
break; |
||||
|
default: |
||||
|
*copied = total_copied; |
||||
|
return FAILURE; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (total_copied > 0) { |
||||
|
*copied = total_copied; |
||||
|
return SUCCESS; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
static zend_result php_io_linux_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
#ifdef HAVE_SENDFILE |
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
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) { |
||||
|
break; |
||||
|
} else { |
||||
|
switch (errno) { |
||||
|
case EINTR: |
||||
|
continue; |
||||
|
case EINVAL: |
||||
|
case ENOSYS: |
||||
|
if (total_copied == 0) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
break; |
||||
|
case EAGAIN: |
||||
|
break; |
||||
|
default: |
||||
|
*copied = total_copied; |
||||
|
return FAILURE; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (total_copied > 0) { |
||||
|
*copied = total_copied; |
||||
|
return SUCCESS; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#ifdef HAVE_SPLICE |
||||
|
/* Enlarge the intermediate pipe so socket transfers move more data per splice |
||||
|
* round-trip. Capped by /proc/sys/fs/pipe-max-size (1 MiB by default); a failed |
||||
|
* fcntl() simply leaves the kernel default (64 KiB) in place. */ |
||||
|
#define PHP_IO_PIPE_SIZE (1 << 20) |
||||
|
|
||||
|
/* Kernel MAX_RW_COUNT; a larger len fails the pos + len overflow check with |
||||
|
* EINVAL once a file destination sits at a non-zero offset. */ |
||||
|
#define PHP_IO_SPLICE_MAX ((size_t) 0x7ffff000) |
||||
|
|
||||
|
/* SPLICE_F_MORE corks the socket the same way MSG_MORE does, letting the kernel |
||||
|
* coalesce splices into full segments. The final partial segment is not flushed |
||||
|
* by the kernel until the cork timer (<= 200 ms) expires or the socket is next |
||||
|
* written/closed, so the copy loops below clear the cork once they are done. */ |
||||
|
static inline unsigned int php_io_linux_out_flags(const php_io_fd *dest) |
||||
|
{ |
||||
|
return (dest->fd_type == PHP_IO_FD_SOCKET) ? SPLICE_F_MORE : 0; |
||||
|
} |
||||
|
|
||||
|
/* Clearing TCP_CORK pushes out any segment still held by SPLICE_F_MORE; |
||||
|
* on non-TCP sockets the setsockopt() harmlessly fails. */ |
||||
|
static inline void php_io_linux_socket_uncork(const php_io_fd *dest, size_t total_copied) |
||||
|
{ |
||||
|
if (dest->fd_type == PHP_IO_FD_SOCKET && total_copied > 0) { |
||||
|
int off = 0; |
||||
|
setsockopt(dest->fd, IPPROTO_TCP, TCP_CORK, &off, sizeof(off)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static zend_result php_io_linux_splice_from_pipe(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
int dest_fd = dest->fd; |
||||
|
unsigned int out_flags = php_io_linux_out_flags(dest); |
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
int ready = php_io_linux_wait_for_data(src); |
||||
|
if (ready == 0) { |
||||
|
break; |
||||
|
} else if (ready < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
size_t to_copy = (remaining < PHP_IO_SPLICE_MAX) ? remaining : PHP_IO_SPLICE_MAX; |
||||
|
ssize_t spliced = splice(src->fd, NULL, dest_fd, NULL, to_copy, out_flags); |
||||
|
|
||||
|
if (spliced > 0) { |
||||
|
total_copied += spliced; |
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= spliced; |
||||
|
} |
||||
|
} else if (spliced == 0) { |
||||
|
break; |
||||
|
} else { |
||||
|
if (total_copied == 0) { |
||||
|
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
php_io_linux_socket_uncork(dest, total_copied); |
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
static zend_result php_io_linux_splice_via_pipe(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
int dest_fd = dest->fd; |
||||
|
unsigned int out_flags = php_io_linux_out_flags(dest); |
||||
|
int pipefd[2]; |
||||
|
if (pipe(pipefd) == -1) { |
||||
|
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#ifdef F_SETPIPE_SZ |
||||
|
fcntl(pipefd[1], F_SETPIPE_SZ, PHP_IO_PIPE_SIZE); |
||||
|
#endif |
||||
|
|
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
int ready = php_io_linux_wait_for_data(src); |
||||
|
if (ready == 0) { |
||||
|
/* timeout */ |
||||
|
break; |
||||
|
} else if (ready < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
size_t to_copy = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; |
||||
|
|
||||
|
ssize_t in_pipe = splice(src->fd, NULL, pipefd[1], NULL, to_copy, 0); |
||||
|
if (in_pipe < 0) { |
||||
|
if (total_copied == 0) { |
||||
|
close(pipefd[0]); |
||||
|
close(pipefd[1]); |
||||
|
return php_io_generic_copy_fallback(src->fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
if (in_pipe == 0) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
size_t pipe_remaining = in_pipe; |
||||
|
while (pipe_remaining > 0) { |
||||
|
ssize_t out = splice(pipefd[0], NULL, dest_fd, NULL, pipe_remaining, out_flags); |
||||
|
if (out <= 0) { |
||||
|
/* the dest refused the splice; salvage what already sits in |
||||
|
* the pipe with a plain read/write loop before failing */ |
||||
|
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; |
||||
|
do { |
||||
|
drained = read(pipefd[0], drain_buf, to_drain); |
||||
|
} while (drained < 0 && errno == EINTR); |
||||
|
if (drained <= 0) { |
||||
|
break; |
||||
|
} |
||||
|
ssize_t drain_written = 0; |
||||
|
while (drain_written < drained) { |
||||
|
ssize_t written; |
||||
|
do { |
||||
|
written = write(dest_fd, drain_buf + drain_written, drained - drain_written); |
||||
|
} while (written < 0 && errno == EINTR); |
||||
|
if (written <= 0) { |
||||
|
total_copied += drain_written; |
||||
|
result = FAILURE; |
||||
|
goto out; |
||||
|
} |
||||
|
drain_written += written; |
||||
|
} |
||||
|
pipe_remaining -= drain_written; |
||||
|
total_copied += drain_written; |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
goto out; |
||||
|
} |
||||
|
pipe_remaining -= out; |
||||
|
total_copied += out; |
||||
|
} |
||||
|
|
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= in_pipe; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
out: |
||||
|
close(pipefd[0]); |
||||
|
close(pipefd[1]); |
||||
|
php_io_linux_socket_uncork(dest, total_copied); |
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
} |
||||
|
#endif /* HAVE_SPLICE */ |
||||
|
|
||||
|
zend_result php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_FILE) { |
||||
|
return php_io_linux_copy_file_to_file(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
return php_io_linux_sendfile(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_PIPE) { |
||||
|
return php_io_linux_sendfile(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#ifdef HAVE_SPLICE |
||||
|
if (src->fd_type == PHP_IO_FD_PIPE) { |
||||
|
return php_io_linux_splice_from_pipe(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
if (src->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
return php_io_linux_splice_via_pipe(src, dest, maxlen, copied); |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
/* php_io_generic_copy honours the stream timeout for socket sources */ |
||||
|
return php_io_generic_copy(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#endif /* __linux__ */ |
||||
@ -0,0 +1,100 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#if defined(__APPLE__) |
||||
|
|
||||
|
#include "php_io_internal.h" |
||||
|
#include <sys/types.h> |
||||
|
#include <sys/socket.h> |
||||
|
#include <sys/uio.h> |
||||
|
#include <unistd.h> |
||||
|
#include <errno.h> |
||||
|
|
||||
|
static zend_result php_io_macos_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
#ifdef HAVE_SENDFILE |
||||
|
/* macOS sendfile() takes an explicit offset and does not advance the |
||||
|
* source descriptor, so remember the starting position and restore it |
||||
|
* afterwards to keep the streams-layer contract. */ |
||||
|
off_t start_offset = lseek(src_fd, 0, SEEK_CUR); |
||||
|
if (start_offset == (off_t) -1) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
off_t total_sent = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? 0 : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (maxlen == PHP_IO_COPY_ALL || remaining > 0) { |
||||
|
/* len is in/out: on input the number of bytes to send (0 means |
||||
|
* until end of file), on output the number of bytes actually sent |
||||
|
* (populated even when sendfile() reports EINTR/EAGAIN). */ |
||||
|
off_t len = (maxlen == PHP_IO_COPY_ALL) ? 0 : (off_t) remaining; |
||||
|
int sendfile_result = sendfile(src_fd, dest_fd, start_offset + total_sent, &len, NULL, 0); |
||||
|
|
||||
|
if (len > 0) { |
||||
|
total_sent += len; |
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= (size_t) len; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (sendfile_result == 0) { |
||||
|
/* Reached EOF or sent the whole requested range. */ |
||||
|
if (len == 0 || maxlen == PHP_IO_COPY_ALL || remaining == 0) { |
||||
|
break; |
||||
|
} |
||||
|
} else { |
||||
|
if (errno == EINTR) { |
||||
|
continue; |
||||
|
} |
||||
|
if (errno == EAGAIN) { |
||||
|
if (len > 0) { |
||||
|
continue; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
if (total_sent == 0) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (total_sent > 0) { |
||||
|
/* best effort: keep reporting the delivered bytes even if this fails */ |
||||
|
lseek(src_fd, start_offset + total_sent, SEEK_SET); |
||||
|
} |
||||
|
|
||||
|
*copied = (size_t) total_sent; |
||||
|
return result; |
||||
|
#else |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
zend_result php_io_macos_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
/* Like FreeBSD, macOS sendfile() works only with a regular file source |
||||
|
* and a socket destination. */ |
||||
|
if (src->fd_type == PHP_IO_FD_FILE && dest->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
return php_io_macos_sendfile(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
/* php_io_generic_copy honours the stream timeout for socket sources */ |
||||
|
return php_io_generic_copy(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#endif |
||||
@ -0,0 +1,104 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#if defined(__sun) && defined(__SVR4) |
||||
|
|
||||
|
#include "php_io_internal.h" |
||||
|
#include <sys/types.h> |
||||
|
#include <sys/stat.h> |
||||
|
#include <sys/sendfile.h> |
||||
|
#include <unistd.h> |
||||
|
#include <errno.h> |
||||
|
|
||||
|
static zend_result php_io_solaris_sendfile(int src_fd, int dest_fd, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
#ifdef HAVE_SENDFILE |
||||
|
/* Solaris/illumos sendfile() takes an explicit offset and does not |
||||
|
* advance the source descriptor, so remember the starting position and |
||||
|
* restore it afterwards to keep the streams-layer contract. */ |
||||
|
off_t start_offset = lseek(src_fd, 0, SEEK_CUR); |
||||
|
if (start_offset == (off_t) -1) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
/* Unlike Linux, Solaris sendfile() returns -1/EINVAL (rather than 0) when |
||||
|
* called with an offset at or past EOF, so a trailing call issued after the |
||||
|
* source has been fully consumed would spuriously fail an otherwise complete |
||||
|
* copy. Bound the loop to the bytes actually available in the source file so |
||||
|
* we stop exactly at EOF and never call sendfile() past it. */ |
||||
|
struct stat st; |
||||
|
if (fstat(src_fd, &st) != 0 || !S_ISREG(st.st_mode)) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
size_t available = (st.st_size > start_offset) ? (size_t) (st.st_size - start_offset) : 0; |
||||
|
size_t target = (maxlen == PHP_IO_COPY_ALL || maxlen > available) ? available : maxlen; |
||||
|
|
||||
|
off_t offset = start_offset; |
||||
|
size_t total_copied = 0; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (total_copied < target) { |
||||
|
size_t remaining = target - total_copied; |
||||
|
size_t to_send = (remaining < SSIZE_MAX) ? remaining : SSIZE_MAX; |
||||
|
/* offset is updated in place by sendfile() */ |
||||
|
ssize_t sent = sendfile(dest_fd, src_fd, &offset, to_send); |
||||
|
|
||||
|
if (sent > 0) { |
||||
|
total_copied += (size_t) sent; |
||||
|
} else if (sent == 0) { |
||||
|
/* Source shrank under us; stop with what we have. */ |
||||
|
break; |
||||
|
} else { |
||||
|
if (errno == EINTR) { |
||||
|
continue; |
||||
|
} |
||||
|
if (errno == EAGAIN) { |
||||
|
break; |
||||
|
} |
||||
|
if (total_copied == 0) { |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
} |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (total_copied > 0) { |
||||
|
/* best effort: keep reporting the delivered bytes even if this fails */ |
||||
|
lseek(src_fd, start_offset + (off_t) total_copied, SEEK_SET); |
||||
|
} |
||||
|
|
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
#else |
||||
|
return php_io_generic_copy_fallback(src_fd, dest_fd, maxlen, copied); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
zend_result php_io_solaris_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
/* Unlike FreeBSD, Solaris/illumos sendfile() accepts both a socket and a |
||||
|
* regular file as the output descriptor, so file->socket and file->file |
||||
|
* can both be offloaded. */ |
||||
|
if (src->fd_type == PHP_IO_FD_FILE && |
||||
|
(dest->fd_type == PHP_IO_FD_SOCKET || dest->fd_type == PHP_IO_FD_FILE)) { |
||||
|
return php_io_solaris_sendfile(src->fd, dest->fd, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
/* php_io_generic_copy honours the stream timeout for socket sources */ |
||||
|
return php_io_generic_copy(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#endif |
||||
@ -0,0 +1,189 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#include "php_io_internal.h" |
||||
|
|
||||
|
#ifdef PHP_WIN32 |
||||
|
|
||||
|
#include <io.h> |
||||
|
#include <winsock2.h> |
||||
|
#include <mswsock.h> |
||||
|
|
||||
|
static inline ssize_t php_io_win_read(php_io_fd *fd, char *buf, int len) |
||||
|
{ |
||||
|
if (fd->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
int result = recv(fd->socket, buf, len, 0); |
||||
|
return (result == SOCKET_ERROR) ? -1 : (ssize_t) result; |
||||
|
} |
||||
|
return (ssize_t) _read(fd->fd, buf, len); |
||||
|
} |
||||
|
|
||||
|
static inline ssize_t php_io_win_write(php_io_fd *fd, const char *buf, int len) |
||||
|
{ |
||||
|
if (fd->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
int result = send(fd->socket, buf, len, 0); |
||||
|
return (result == SOCKET_ERROR) ? -1 : (ssize_t) result; |
||||
|
} |
||||
|
return (ssize_t) _write(fd->fd, buf, len); |
||||
|
} |
||||
|
|
||||
|
static inline int php_io_win_wait_for_data(php_io_fd *fd) |
||||
|
{ |
||||
|
if (fd->fd_type != PHP_IO_FD_SOCKET || !fd->is_blocked) { |
||||
|
return 1; |
||||
|
} |
||||
|
|
||||
|
int timeout_ms; |
||||
|
if (fd->timeout.tv_sec == -1) { |
||||
|
timeout_ms = -1; |
||||
|
} else { |
||||
|
timeout_ms = fd->timeout.tv_sec * 1000 + fd->timeout.tv_usec / 1000; |
||||
|
} |
||||
|
|
||||
|
WSAPOLLFD pfd; |
||||
|
pfd.fd = fd->socket; |
||||
|
pfd.events = POLLIN; |
||||
|
|
||||
|
int ret; |
||||
|
do { |
||||
|
ret = WSAPoll(&pfd, 1, timeout_ms); |
||||
|
} while (ret == SOCKET_ERROR && WSAGetLastError() == WSAEINTR); |
||||
|
|
||||
|
return (ret == SOCKET_ERROR) ? -1 : ret; |
||||
|
} |
||||
|
|
||||
|
static zend_result php_io_win_copy_loop(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
char buf[PHP_IO_COPY_BUFSIZE]; |
||||
|
size_t total_copied = 0; |
||||
|
size_t remaining = (maxlen == PHP_IO_COPY_ALL) ? SIZE_MAX : maxlen; |
||||
|
zend_result result = SUCCESS; |
||||
|
|
||||
|
while (remaining > 0) { |
||||
|
int ready = php_io_win_wait_for_data(src); |
||||
|
if (ready == 0) { |
||||
|
break; |
||||
|
} else if (ready < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
int to_read = (remaining < sizeof(buf)) ? (int) remaining : (int) sizeof(buf); |
||||
|
ssize_t bytes_read = php_io_win_read(src, buf, to_read); |
||||
|
|
||||
|
if (bytes_read < 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} else if (bytes_read == 0) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
const char *writeptr = buf; |
||||
|
size_t to_write = (size_t) bytes_read; |
||||
|
|
||||
|
while (to_write > 0) { |
||||
|
ssize_t bytes_written = php_io_win_write(dest, writeptr, (int) to_write); |
||||
|
if (bytes_written <= 0) { |
||||
|
result = FAILURE; |
||||
|
break; |
||||
|
} |
||||
|
total_copied += bytes_written; |
||||
|
writeptr += bytes_written; |
||||
|
to_write -= bytes_written; |
||||
|
} |
||||
|
if (result == FAILURE) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
if (maxlen != PHP_IO_COPY_ALL) { |
||||
|
remaining -= bytes_read; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
*copied = total_copied; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/* Documented TransmitFile per-call max; larger requests get WSAEINVAL, so they |
||||
|
* fall back to the read/write loop. */ |
||||
|
#define PHP_IO_WIN_TRANSMIT_MAX ((size_t) 2147483646) |
||||
|
|
||||
|
/* Negative results from php_io_win_transmit_file(). */ |
||||
|
#define PHP_IO_WIN_TRANSMIT_FALLBACK ((ssize_t) -1) /* nothing sent; retry via loop */ |
||||
|
#define PHP_IO_WIN_TRANSMIT_ERROR ((ssize_t) -2) /* may have sent data; do not retry */ |
||||
|
|
||||
|
static ssize_t php_io_win_transmit_file(int src_fd, SOCKET dest_sock, size_t maxlen) |
||||
|
{ |
||||
|
HANDLE file_handle = (HANDLE) _get_osfhandle(src_fd); |
||||
|
|
||||
|
if (file_handle == INVALID_HANDLE_VALUE || |
||||
|
dest_sock == INVALID_SOCKET || |
||||
|
GetFileType(file_handle) != FILE_TYPE_DISK) { |
||||
|
return PHP_IO_WIN_TRANSMIT_FALLBACK; |
||||
|
} |
||||
|
|
||||
|
LARGE_INTEGER file_pos, file_size; |
||||
|
file_pos.QuadPart = 0; |
||||
|
if (!SetFilePointerEx(file_handle, file_pos, &file_pos, FILE_CURRENT)) { |
||||
|
return PHP_IO_WIN_TRANSMIT_FALLBACK; |
||||
|
} |
||||
|
|
||||
|
if (!GetFileSizeEx(file_handle, &file_size)) { |
||||
|
return PHP_IO_WIN_TRANSMIT_FALLBACK; |
||||
|
} |
||||
|
|
||||
|
LONGLONG available = file_size.QuadPart - file_pos.QuadPart; |
||||
|
if (available <= 0) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
/* 64-bit compare avoids truncating maxlen into the DWORD arg below */ |
||||
|
ULONGLONG to_send = (maxlen == PHP_IO_COPY_ALL || (ULONGLONG) maxlen > (ULONGLONG) available) |
||||
|
? (ULONGLONG) available : (ULONGLONG) maxlen; |
||||
|
|
||||
|
if (to_send > PHP_IO_WIN_TRANSMIT_MAX) { |
||||
|
return PHP_IO_WIN_TRANSMIT_FALLBACK; |
||||
|
} |
||||
|
|
||||
|
if (!TransmitFile(dest_sock, file_handle, (DWORD) to_send, 0, NULL, NULL, 0)) { |
||||
|
/* may have sent data, so don't resend via the loop */ |
||||
|
return PHP_IO_WIN_TRANSMIT_ERROR; |
||||
|
} |
||||
|
|
||||
|
LARGE_INTEGER advance; |
||||
|
advance.QuadPart = (LONGLONG) to_send; |
||||
|
SetFilePointerEx(file_handle, advance, NULL, FILE_CURRENT); |
||||
|
|
||||
|
return (ssize_t) to_send; |
||||
|
} |
||||
|
|
||||
|
zend_result php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) |
||||
|
{ |
||||
|
if (src->fd_type != PHP_IO_FD_SOCKET && dest->fd_type == PHP_IO_FD_SOCKET) { |
||||
|
ssize_t result = php_io_win_transmit_file(src->fd, dest->socket, maxlen); |
||||
|
if (result >= 0) { |
||||
|
*copied = (size_t) result; |
||||
|
return SUCCESS; |
||||
|
} |
||||
|
if (result == PHP_IO_WIN_TRANSMIT_ERROR) { |
||||
|
*copied = 0; |
||||
|
return FAILURE; |
||||
|
} |
||||
|
/* PHP_IO_WIN_TRANSMIT_FALLBACK: nothing was sent, copy via the loop. */ |
||||
|
} |
||||
|
|
||||
|
return php_io_win_copy_loop(src, dest, maxlen, copied); |
||||
|
} |
||||
|
|
||||
|
#endif /* PHP_WIN32 */ |
||||
@ -0,0 +1,27 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Copyright (c) The PHP Group | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_FREEBSD_H |
||||
|
#define PHP_IO_FREEBSD_H |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_freebsd_copy |
||||
|
#if defined(__DragonFly__) |
||||
|
# define PHP_IO_PLATFORM_NAME "dragonfly" |
||||
|
#else |
||||
|
# define PHP_IO_PLATFORM_NAME "freebsd" |
||||
|
#endif |
||||
|
|
||||
|
zend_result php_io_freebsd_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#endif /* PHP_IO_FREEBSD_H */ |
||||
@ -0,0 +1,21 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Copyright (c) The PHP Group | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_GENERIC_H |
||||
|
#define PHP_IO_GENERIC_H |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_generic_copy |
||||
|
#define PHP_IO_PLATFORM_NAME "generic" |
||||
|
|
||||
|
#endif /* PHP_IO_GENERIC_H */ |
||||
@ -0,0 +1,41 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Copyright (c) The PHP Group | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_INTERNAL_H |
||||
|
#define PHP_IO_INTERNAL_H |
||||
|
|
||||
|
#include "php_io.h" |
||||
|
|
||||
|
/* Buffer size for the userspace read/write copy loops. Larger than the stream |
||||
|
* layer CHUNK_SIZE (8 KiB) to cut the number of syscalls on bulk transfers. */ |
||||
|
#define PHP_IO_COPY_BUFSIZE (64 * 1024) |
||||
|
|
||||
|
zend_result php_io_generic_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
zend_result php_io_generic_copy_fallback(int src_fd, int dest_fd, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#ifdef __linux__ |
||||
|
#include "php_io_linux.h" |
||||
|
#elif defined(PHP_WIN32) |
||||
|
#include "php_io_windows.h" |
||||
|
#elif defined(__FreeBSD__) || defined(__DragonFly__) |
||||
|
#include "php_io_freebsd.h" |
||||
|
#elif defined(__sun) && defined(__SVR4) |
||||
|
#include "php_io_solaris.h" |
||||
|
#elif defined(__APPLE__) |
||||
|
#include "php_io_macos.h" |
||||
|
#else |
||||
|
#include "php_io_generic.h" |
||||
|
#endif |
||||
|
|
||||
|
#endif /* PHP_IO_INTERNAL_H */ |
||||
@ -0,0 +1,23 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_LINUX_H |
||||
|
#define PHP_IO_LINUX_H |
||||
|
|
||||
|
zend_result php_io_linux_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_linux_copy |
||||
|
#define PHP_IO_PLATFORM_NAME "linux" |
||||
|
|
||||
|
#endif /* PHP_IO_LINUX_H */ |
||||
@ -0,0 +1,23 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Copyright (c) The PHP Group | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_MACOS_H |
||||
|
#define PHP_IO_MACOS_H |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_macos_copy |
||||
|
#define PHP_IO_PLATFORM_NAME "macos" |
||||
|
|
||||
|
zend_result php_io_macos_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#endif /* PHP_IO_MACOS_H */ |
||||
@ -0,0 +1,23 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: David Carlier <devnexen@gmail.com> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_SOLARIS_H |
||||
|
#define PHP_IO_SOLARIS_H |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_solaris_copy |
||||
|
#define PHP_IO_PLATFORM_NAME "solaris" |
||||
|
|
||||
|
zend_result php_io_solaris_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#endif /* PHP_IO_SOLARIS_H */ |
||||
@ -0,0 +1,23 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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 <https://www.php.net/license/>. | |
||||
|
| | |
||||
|
| SPDX-License-Identifier: BSD-3-Clause | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| Authors: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_WINDOWS_H |
||||
|
#define PHP_IO_WINDOWS_H |
||||
|
|
||||
|
zend_result php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#define PHP_IO_PLATFORM_COPY php_io_windows_copy |
||||
|
#define PHP_IO_PLATFORM_NAME "windows" |
||||
|
|
||||
|
#endif /* PHP_IO_WINDOWS_H */ |
||||
@ -0,0 +1,53 @@ |
|||||
|
/* |
||||
|
+----------------------------------------------------------------------+ |
||||
|
| 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: Jakub Zelenka <bukka@php.net> | |
||||
|
+----------------------------------------------------------------------+ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef PHP_IO_H |
||||
|
#define PHP_IO_H |
||||
|
|
||||
|
#include "php.h" |
||||
|
#include "php_network.h" |
||||
|
|
||||
|
#define PHP_IO_COPY_ALL SIZE_MAX |
||||
|
|
||||
|
typedef enum php_io_fd_type { |
||||
|
PHP_IO_FD_FILE = 1, |
||||
|
PHP_IO_FD_SOCKET, |
||||
|
PHP_IO_FD_PIPE, |
||||
|
} php_io_fd_type; |
||||
|
|
||||
|
typedef struct php_io_fd { |
||||
|
union { |
||||
|
int fd; |
||||
|
php_socket_t socket; |
||||
|
}; |
||||
|
php_io_fd_type fd_type; |
||||
|
struct timeval timeout; |
||||
|
unsigned is_blocked:1; |
||||
|
} php_io_fd; |
||||
|
|
||||
|
typedef zend_result (*php_io_copy_fn)(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
typedef struct php_io { |
||||
|
php_io_copy_fn copy; |
||||
|
const char *platform_name; |
||||
|
} php_io; |
||||
|
|
||||
|
PHPAPI php_io *php_io_get(void); |
||||
|
|
||||
|
/* Copies up to maxlen bytes from src to dest; *copied is set even on FAILURE */ |
||||
|
PHPAPI zend_result php_io_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); |
||||
|
|
||||
|
#endif /* PHP_IO_H */ |
||||
@ -0,0 +1,293 @@ |
|||||
|
#include "php.h" |
||||
|
#include "io/php_io_internal.h" |
||||
|
#include <fcntl.h> |
||||
|
#include <sys/socket.h> |
||||
|
#include <netinet/in.h> |
||||
|
#include <netinet/tcp.h> |
||||
|
#include <cmocka.h> |
||||
|
|
||||
|
/* Mocked syscalls return the value queued via will_return(); a negative value |
||||
|
* -E is translated to a -1 return with errno set to E. Must expand inside the |
||||
|
* wrapper body because cmocka keys the queued values by __func__. */ |
||||
|
#define MOCK_IO_RESULT(result_var) \ |
||||
|
ssize_t result_var = mock_type(ssize_t); \ |
||||
|
if (result_var < 0) { \ |
||||
|
errno = (int) -result_var; \ |
||||
|
result_var = -1; \ |
||||
|
} |
||||
|
|
||||
|
ssize_t __wrap_copy_file_range(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) |
||||
|
{ |
||||
|
function_called(); |
||||
|
MOCK_IO_RESULT(result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
ssize_t __wrap_sendfile(int out_fd, int in_fd, off_t *offset, size_t count) |
||||
|
{ |
||||
|
function_called(); |
||||
|
MOCK_IO_RESULT(result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
ssize_t __wrap_splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out, size_t len, unsigned int flags) |
||||
|
{ |
||||
|
function_called(); |
||||
|
check_expected(len); |
||||
|
check_expected(flags); |
||||
|
MOCK_IO_RESULT(result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
ssize_t __wrap_read(int fd, void *buf, size_t count) |
||||
|
{ |
||||
|
function_called(); |
||||
|
MOCK_IO_RESULT(result); |
||||
|
if (result > 0) { |
||||
|
memset(buf, 'x', result); |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
ssize_t __wrap_write(int fd, const void *buf, size_t count) |
||||
|
{ |
||||
|
function_called(); |
||||
|
MOCK_IO_RESULT(result); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
int __wrap_poll(struct pollfd *ufds, nfds_t nfds, int timeout) |
||||
|
{ |
||||
|
function_called(); |
||||
|
check_expected(timeout); |
||||
|
|
||||
|
int n = mock_type(int); |
||||
|
if (n > 0) { |
||||
|
ufds->revents = POLLIN; |
||||
|
} else if (n < 0) { |
||||
|
errno = -n; |
||||
|
n = -1; |
||||
|
} |
||||
|
|
||||
|
return n; |
||||
|
} |
||||
|
|
||||
|
int __wrap_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen) |
||||
|
{ |
||||
|
function_called(); |
||||
|
check_expected(optname); |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
/* Kernel MAX_RW_COUNT, must match PHP_IO_SPLICE_MAX in php_io_copy_linux.c */ |
||||
|
#define TEST_SPLICE_MAX ((size_t) 0x7ffff000) |
||||
|
|
||||
|
static php_io_fd make_io_fd(int fd, php_io_fd_type fd_type) |
||||
|
{ |
||||
|
php_io_fd io_fd = { |
||||
|
.fd = fd, |
||||
|
.fd_type = fd_type, |
||||
|
.timeout = { .tv_sec = -1, .tv_usec = 0 }, |
||||
|
.is_blocked = 0, |
||||
|
}; |
||||
|
return io_fd; |
||||
|
} |
||||
|
|
||||
|
/* file -> file: a hard error after partial progress must report FAILURE |
||||
|
* together with the bytes already copied */ |
||||
|
static void test_file_to_file_error_after_partial(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_calls(__wrap_copy_file_range, 2); |
||||
|
will_return(__wrap_copy_file_range, 4096); |
||||
|
will_return(__wrap_copy_file_range, -ENOSPC); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE); |
||||
|
assert_int_equal(copied, 4096); |
||||
|
} |
||||
|
|
||||
|
/* file -> file: EXDEV with no progress falls back to the read/write loop */ |
||||
|
static void test_file_to_file_exdev_fallback(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_call(__wrap_copy_file_range); |
||||
|
will_return(__wrap_copy_file_range, -EXDEV); |
||||
|
|
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 100); |
||||
|
expect_function_call(__wrap_write); |
||||
|
will_return(__wrap_write, 100); |
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 0); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS); |
||||
|
assert_int_equal(copied, 100); |
||||
|
} |
||||
|
|
||||
|
/* the generic fallback loop must retry interrupted read() and write() */ |
||||
|
static void test_generic_fallback_eintr_retry(void **state) |
||||
|
{ |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_calls(__wrap_read, 2); |
||||
|
will_return(__wrap_read, -EINTR); |
||||
|
will_return(__wrap_read, 50); |
||||
|
expect_function_calls(__wrap_write, 2); |
||||
|
will_return(__wrap_write, -EINTR); |
||||
|
will_return(__wrap_write, 50); |
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 0); |
||||
|
|
||||
|
assert_int_equal(php_io_generic_copy_fallback(10, 11, PHP_IO_COPY_ALL, &copied), SUCCESS); |
||||
|
assert_int_equal(copied, 50); |
||||
|
} |
||||
|
|
||||
|
/* the generic fallback loop must report FAILURE with the partial count when a |
||||
|
* write fails mid-copy */ |
||||
|
static void test_generic_fallback_write_error_after_partial(void **state) |
||||
|
{ |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 50); |
||||
|
expect_function_call(__wrap_write); |
||||
|
will_return(__wrap_write, 50); |
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 50); |
||||
|
expect_function_call(__wrap_write); |
||||
|
will_return(__wrap_write, -ENOSPC); |
||||
|
|
||||
|
assert_int_equal(php_io_generic_copy_fallback(10, 11, PHP_IO_COPY_ALL, &copied), FAILURE); |
||||
|
assert_int_equal(copied, 50); |
||||
|
} |
||||
|
|
||||
|
/* file -> socket: a hard sendfile error after partial progress must report |
||||
|
* FAILURE together with the bytes already sent */ |
||||
|
static void test_file_to_socket_sendfile_error_after_partial(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_calls(__wrap_sendfile, 2); |
||||
|
will_return(__wrap_sendfile, 8192); |
||||
|
will_return(__wrap_sendfile, -EPIPE); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE); |
||||
|
assert_int_equal(copied, 8192); |
||||
|
} |
||||
|
|
||||
|
/* file -> socket: sendfile EINVAL with no progress falls back to the |
||||
|
* read/write loop */ |
||||
|
static void test_file_to_socket_sendfile_einval_fallback(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_FILE); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_call(__wrap_sendfile); |
||||
|
will_return(__wrap_sendfile, -EINVAL); |
||||
|
|
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 10); |
||||
|
expect_function_call(__wrap_write); |
||||
|
will_return(__wrap_write, 10); |
||||
|
expect_function_call(__wrap_read); |
||||
|
will_return(__wrap_read, 0); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS); |
||||
|
assert_int_equal(copied, 10); |
||||
|
} |
||||
|
|
||||
|
/* socket -> file: a blocking source socket must be polled with the stream |
||||
|
* timeout and a timeout is a clean stop, not an error */ |
||||
|
static void test_socket_source_poll_timeout(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_SOCKET); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE); |
||||
|
size_t copied = 42; |
||||
|
|
||||
|
src.is_blocked = 1; |
||||
|
src.timeout.tv_sec = 2; |
||||
|
src.timeout.tv_usec = 500000; |
||||
|
|
||||
|
expect_function_call(__wrap_poll); |
||||
|
expect_value(__wrap_poll, timeout, 2500); |
||||
|
will_return(__wrap_poll, 0); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), SUCCESS); |
||||
|
assert_int_equal(copied, 0); |
||||
|
} |
||||
|
|
||||
|
/* pipe -> socket: splices are corked with SPLICE_F_MORE and capped at the |
||||
|
* kernel MAX_RW_COUNT; an error after partial progress must report FAILURE |
||||
|
* with the partial count and still clear the cork */ |
||||
|
static void test_pipe_to_socket_splice_error_after_partial_uncorks(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_PIPE); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_SOCKET); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_calls(__wrap_splice, 2); |
||||
|
expect_value_count(__wrap_splice, len, TEST_SPLICE_MAX, 2); |
||||
|
expect_value_count(__wrap_splice, flags, SPLICE_F_MORE, 2); |
||||
|
will_return(__wrap_splice, 1000); |
||||
|
will_return(__wrap_splice, -EPIPE); |
||||
|
|
||||
|
expect_function_call(__wrap_setsockopt); |
||||
|
expect_value(__wrap_setsockopt, optname, TCP_CORK); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE); |
||||
|
assert_int_equal(copied, 1000); |
||||
|
} |
||||
|
|
||||
|
/* socket -> file: when the outbound splice fails, the data already sitting in |
||||
|
* the intermediate pipe is salvaged with a read/write loop (retrying EINTR) |
||||
|
* and the copy reports FAILURE with the salvaged count */ |
||||
|
static void test_socket_to_file_splice_out_error_drains_pipe(void **state) |
||||
|
{ |
||||
|
php_io_fd src = make_io_fd(10, PHP_IO_FD_SOCKET); |
||||
|
php_io_fd dest = make_io_fd(11, PHP_IO_FD_FILE); |
||||
|
size_t copied = 0; |
||||
|
|
||||
|
expect_function_calls(__wrap_splice, 2); |
||||
|
/* socket -> pipe */ |
||||
|
expect_value(__wrap_splice, len, SSIZE_MAX); |
||||
|
expect_value(__wrap_splice, flags, 0); |
||||
|
will_return(__wrap_splice, 500); |
||||
|
/* pipe -> file */ |
||||
|
expect_value(__wrap_splice, len, 500); |
||||
|
expect_value(__wrap_splice, flags, 0); |
||||
|
will_return(__wrap_splice, -EINVAL); |
||||
|
|
||||
|
expect_function_calls(__wrap_read, 2); |
||||
|
will_return(__wrap_read, -EINTR); |
||||
|
will_return(__wrap_read, 500); |
||||
|
expect_function_call(__wrap_write); |
||||
|
will_return(__wrap_write, 500); |
||||
|
|
||||
|
assert_int_equal(php_io_linux_copy(&src, &dest, PHP_IO_COPY_ALL, &copied), FAILURE); |
||||
|
assert_int_equal(copied, 500); |
||||
|
} |
||||
|
|
||||
|
int main(void) |
||||
|
{ |
||||
|
const struct CMUnitTest tests[] = { |
||||
|
cmocka_unit_test(test_file_to_file_error_after_partial), |
||||
|
cmocka_unit_test(test_file_to_file_exdev_fallback), |
||||
|
cmocka_unit_test(test_generic_fallback_eintr_retry), |
||||
|
cmocka_unit_test(test_generic_fallback_write_error_after_partial), |
||||
|
cmocka_unit_test(test_file_to_socket_sendfile_error_after_partial), |
||||
|
cmocka_unit_test(test_file_to_socket_sendfile_einval_fallback), |
||||
|
cmocka_unit_test(test_socket_source_poll_timeout), |
||||
|
cmocka_unit_test(test_pipe_to_socket_splice_error_after_partial_uncorks), |
||||
|
cmocka_unit_test(test_socket_to_file_splice_out_error_drains_pipe), |
||||
|
}; |
||||
|
return cmocka_run_group_tests(tests, NULL, NULL); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue