stream_copy_to_stream's $offset argument is unclear
Description
imo it should have been null-by-default since the beginning, only seeking if the user want it to seek for some reason, but i suspect that would be too much of a BC-break to fix today, however, $offset should be nullable, i should be able to do
stream_copy_to_stream(... offset:null);
instead of
stream_copy_to_stream(... offset: ftell($in));
I think you're misunderstanding something.
The offset argument is the offset from the current position. So if you put offset 0 then it already is from the position ftell would give you. So putting offset: ftell($in) doesn't make sense because let's say you're already at offset 10, then ftell will also return 10 and now the actual offset in the file will be offset 10+10=20.
See the following demo program for example:
<?php
file_put_contents("mytmpfile.tmp", "hello world!");
$input = fopen("mytmpfile.tmp", "r");
$output = fopen("mytmpoutput.tmp", "w");
fseek($input, 6);
stream_copy_to_stream($input, $output, 5, offset: 0);
fclose($input);
fclose($output);
This will create an output file containing "world" because the "hello " was already skipped by the fseek, I didn't have to use ftell here.
Maybe this should be categorised as a documentation problem? Let me know if something is still unclear.
ah yeah you're right, thanks. the documentation could probably be better, i got confused.. so stream_get_content()'s $offset actually seeks from an absolute position (akin to a fseek(SEEK_SET)), while stream_copy_to_stream()'s $offset seeks from a relative position (akin to fseek(SEEK_CUR)), but kind-of worse, because it can only "seek" forward.. peculiar
(as a nitpick, when you just need temporary files for something, you should use tmpfile() not fopen() ^^ and if you need temporary file's path, you can do $handle = tmpfile(); $path = stream_get_meta_data($handle)['uri']; this guarantee unique temp files, and automatically clean up the temp files on exit because tmpfile()'s are automatically deleted on exit, and also makes the code 3v4l.org compatible: https://3v4l.org/INcWP )
I transferred this issue to the documentation repo.
The TL;DR is basically in the comment above this one: the behaviour of $offset is unclear in the docs because it is not consistent with the other $offset in stream_get_content. The documentation should clarify that the $offset is relative to the current file offset.
RE: tmp files. Right, I forgot about this :sweat_smile: Thanks for reminding me :p
The offset is not relative to the current offset - at least it doesn't seem like it if we're at EOF. If I have a 12 bytes stream and use stream_get_contents without any param, the pointer is at the end of the file eventually. stream_copy_to_stream after that without offset or length returns 0 and nothing is copied. If I specify 1 as offset, it returns 11 and copies all but the first byte so specifying 1 behaves as if the pointer is set to the absolute position 1. Even if I set offset to -1 it still doesn't copy anything and returns 0. So the only way to always copy everything is to manually rewind the input stream which behaves differently to stream_get_contents which has offset -1 as default and passing 0 would seek to the absolute position 0.
Yeah, $offset does a SEEK_SET. But also only if it's >0. Not sure why it wasn't made nullable like $length...
https://github.com/php/php-src/blob/php-8.4.3/ext/standard/streamsfuncs.c#L518
Here's a permalink to the lines mentioned by @damianwadley for future viewers to reference:
if (pos > 0 && php_stream_seek(src, pos, SEEK_SET) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to seek to position " ZEND_LONG_FMT " in the stream", pos);
RETURN_FALSE;
}
https://github.com/php/php-src/blob/914f9ad49be800e887b4c5df637ff9af02eb5eeb/ext/standard/streamsfuncs.c#L498-L501
TL;DR: stream_copy_to_stream($from, $to, $length, $offset) is equivalent to fseek($from, $offset, SEEK_SET) followed by stream_copy_to_stream($from, $to, $length, $offset) followed by stream_copy_to_stream($from, $to, $length, $offset) IF (and only IF) $length > 0. See https://www.php.net/manual/en/function.fseek.php for further reading.
Gotcha 1: Beware using a variable $length with a previously used stream! If your $length variable is 0 but the stream was previously used/seeked to a non-zero position, then stream_copy_to_stream will ignore your $length and copy from the current position.
Gotcha 2: Failure to seek the specified location is an error, not an warning. Check the output of stream_copy_to_stream for false to see if it failed.