doc-en icon indicating copy to clipboard operation
doc-en copied to clipboard

stream_copy_to_stream's $offset argument is unclear

Open divinity76 opened this issue 2 years ago • 5 comments

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));

divinity76 avatar Apr 09 '23 11:04 divinity76

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.

ndossche avatar Apr 09 '23 13:04 ndossche

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 )

divinity76 avatar Apr 09 '23 14:04 divinity76

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

ndossche avatar Apr 09 '23 17:04 ndossche

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.

smares avatar Feb 04 '25 15:02 smares

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

damianwadley avatar Feb 04 '25 16:02 damianwadley

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.

anonyco avatar Aug 30 '25 17:08 anonyco