/src/php-src/main/streams/cast.c
Line | Count | Source |
1 | | /* |
2 | | +----------------------------------------------------------------------+ |
3 | | | Copyright © The PHP Group and Contributors. | |
4 | | +----------------------------------------------------------------------+ |
5 | | | This source file is subject to the Modified BSD License that is | |
6 | | | bundled with this package in the file LICENSE, and is available | |
7 | | | through the World Wide Web at <https://www.php.net/license/>. | |
8 | | | | |
9 | | | SPDX-License-Identifier: BSD-3-Clause | |
10 | | +----------------------------------------------------------------------+ |
11 | | | Authors: Wez Furlong <wez@thebrainroom.com> | |
12 | | +----------------------------------------------------------------------+ |
13 | | */ |
14 | | |
15 | | #ifndef _GNU_SOURCE |
16 | | # define _GNU_SOURCE |
17 | | #endif |
18 | | #include "php.h" |
19 | | #include "php_globals.h" |
20 | | #include "php_network.h" |
21 | | #include "php_open_temporary_file.h" |
22 | | #include "ext/standard/file.h" |
23 | | #include <stddef.h> |
24 | | #include <fcntl.h> |
25 | | |
26 | | #include "php_streams_int.h" |
27 | | |
28 | | /* Under BSD, emulate fopencookie using funopen */ |
29 | | #if defined(HAVE_FUNOPEN) && !defined(HAVE_FOPENCOOKIE) |
30 | | |
31 | | /* NetBSD 6.0+ uses off_t instead of fpos_t in funopen */ |
32 | | # if defined(__NetBSD__) && (__NetBSD_Version__ >= 600000000) |
33 | | # define PHP_FPOS_T off_t |
34 | | # else |
35 | | # define PHP_FPOS_T fpos_t |
36 | | # endif |
37 | | |
38 | | typedef struct { |
39 | | int (*reader)(void *, char *, int); |
40 | | int (*writer)(void *, const char *, int); |
41 | | PHP_FPOS_T (*seeker)(void *, PHP_FPOS_T, int); |
42 | | int (*closer)(void *); |
43 | | } cookie_io_functions_t; |
44 | | |
45 | | FILE *fopencookie(void *cookie, const char *mode, cookie_io_functions_t *funcs) |
46 | | { |
47 | | FILE *file = funopen(cookie, funcs->reader, funcs->writer, funcs->seeker, funcs->closer); |
48 | | if (file) { |
49 | | /* Buffering of FILE handles is stateful. |
50 | | * A bailout during these can corrupt the state of the FILE handle |
51 | | * and cause memory corruption errors. See GH-11078. */ |
52 | | setvbuf(file, NULL, _IONBF, 0); |
53 | | } |
54 | | return file; |
55 | | } |
56 | | # define HAVE_FOPENCOOKIE 1 |
57 | | # define PHP_EMULATE_FOPENCOOKIE 1 |
58 | | # define PHP_STREAM_COOKIE_FUNCTIONS &stream_cookie_functions |
59 | | #elif defined(HAVE_FOPENCOOKIE) |
60 | 0 | # define PHP_STREAM_COOKIE_FUNCTIONS stream_cookie_functions |
61 | | #endif |
62 | | |
63 | | /* {{{ STDIO with fopencookie */ |
64 | | #if defined(PHP_EMULATE_FOPENCOOKIE) |
65 | | /* use our fopencookie emulation */ |
66 | | static int stream_cookie_reader(void *cookie, char *buffer, int size) |
67 | | { |
68 | | int ret; |
69 | | |
70 | | ret = php_stream_read((php_stream*)cookie, buffer, size); |
71 | | return ret; |
72 | | } |
73 | | |
74 | | static int stream_cookie_writer(void *cookie, const char *buffer, int size) |
75 | | { |
76 | | |
77 | | return php_stream_write((php_stream *)cookie, (char *)buffer, size); |
78 | | } |
79 | | |
80 | | static PHP_FPOS_T stream_cookie_seeker(void *cookie, zend_off_t position, int whence) |
81 | | { |
82 | | |
83 | | return (PHP_FPOS_T)php_stream_seek((php_stream *)cookie, position, whence); |
84 | | } |
85 | | |
86 | | static int stream_cookie_closer(void *cookie) |
87 | | { |
88 | | php_stream *stream = (php_stream*)cookie; |
89 | | |
90 | | /* prevent recursion */ |
91 | | stream->fclose_stdiocast = PHP_STREAM_FCLOSE_NONE; |
92 | | return php_stream_free(stream, |
93 | | PHP_STREAM_FREE_CLOSE | PHP_STREAM_FREE_KEEP_RSRC | PHP_STREAM_FREE_RSRC_DTOR); |
94 | | } |
95 | | #elif defined(HAVE_FOPENCOOKIE) |
96 | | static ssize_t stream_cookie_reader(void *cookie, char *buffer, size_t size) |
97 | 0 | { |
98 | 0 | ssize_t ret; |
99 | |
|
100 | 0 | ret = php_stream_read(((php_stream *)cookie), buffer, size); |
101 | 0 | return ret; |
102 | 0 | } |
103 | | |
104 | | static ssize_t stream_cookie_writer(void *cookie, const char *buffer, size_t size) |
105 | 0 | { |
106 | |
|
107 | 0 | return php_stream_write(((php_stream *)cookie), (char *)buffer, size); |
108 | 0 | } |
109 | | |
110 | | # ifdef COOKIE_SEEKER_USES_OFF64_T |
111 | | static int stream_cookie_seeker(void *cookie, off64_t *position, int whence) |
112 | | # else |
113 | | static int stream_cookie_seeker(void *cookie, off_t *position, int whence) |
114 | | # endif |
115 | 0 | { |
116 | |
|
117 | 0 | *position = php_stream_seek((php_stream *)cookie, (zend_off_t)*position, whence); |
118 | |
|
119 | 0 | if (*position == -1) { |
120 | 0 | return -1; |
121 | 0 | } |
122 | 0 | return 0; |
123 | 0 | } |
124 | | |
125 | | static int stream_cookie_closer(void *cookie) |
126 | 0 | { |
127 | 0 | php_stream *stream = (php_stream*)cookie; |
128 | | |
129 | | /* prevent recursion */ |
130 | 0 | stream->fclose_stdiocast = PHP_STREAM_FCLOSE_NONE; |
131 | 0 | return php_stream_free(stream, |
132 | 0 | PHP_STREAM_FREE_CLOSE | PHP_STREAM_FREE_KEEP_RSRC | PHP_STREAM_FREE_RSRC_DTOR); |
133 | 0 | } |
134 | | #endif /* elif defined(HAVE_FOPENCOOKIE) */ |
135 | | |
136 | | #ifdef HAVE_FOPENCOOKIE |
137 | | static cookie_io_functions_t stream_cookie_functions = |
138 | | { |
139 | | stream_cookie_reader, stream_cookie_writer, |
140 | | stream_cookie_seeker, stream_cookie_closer |
141 | | }; |
142 | | #else |
143 | | /* TODO: use socketpair() to emulate fopencookie, as suggested by Hartmut ? */ |
144 | | #endif |
145 | | /* }}} */ |
146 | | |
147 | | /* {{{ php_stream_mode_sanitize_fdopen_fopencookie |
148 | | * Result should have at least size 5, e.g. to write wbx+\0 */ |
149 | | void php_stream_mode_sanitize_fdopen_fopencookie(php_stream *stream, char *result) |
150 | 0 | { |
151 | | /* replace modes not supported by fdopen and fopencookie, but supported |
152 | | * by PHP's fread(), so that their calls won't fail */ |
153 | 0 | const char *cur_mode = stream->mode; |
154 | 0 | int has_plus = 0, |
155 | 0 | has_bin = 0, |
156 | 0 | i, |
157 | 0 | res_curs = 0; |
158 | |
|
159 | 0 | if (cur_mode[0] == 'r' || cur_mode[0] == 'w' || cur_mode[0] == 'a') { |
160 | 0 | result[res_curs++] = cur_mode[0]; |
161 | 0 | } else { |
162 | | /* assume cur_mode[0] is 'c' or 'x'; substitute by 'w', which should not |
163 | | * truncate anything in fdopen/fopencookie */ |
164 | 0 | result[res_curs++] = 'w'; |
165 | | |
166 | | /* x is allowed (at least by glibc & compat), but not as the 1st mode |
167 | | * as in PHP and in any case is (at best) ignored by fdopen and fopencookie */ |
168 | 0 | } |
169 | | |
170 | | /* assume current mode has at most length 4 (e.g. wbn+) */ |
171 | 0 | for (i = 1; i < 4 && cur_mode[i] != '\0'; i++) { |
172 | 0 | if (cur_mode[i] == 'b') { |
173 | 0 | has_bin = 1; |
174 | 0 | } else if (cur_mode[i] == '+') { |
175 | 0 | has_plus = 1; |
176 | 0 | } |
177 | | /* ignore 'n', 't' or other stuff */ |
178 | 0 | } |
179 | |
|
180 | 0 | if (has_bin) { |
181 | 0 | result[res_curs++] = 'b'; |
182 | 0 | } |
183 | 0 | if (has_plus) { |
184 | 0 | result[res_curs++] = '+'; |
185 | 0 | } |
186 | |
|
187 | 0 | result[res_curs] = '\0'; |
188 | 0 | } |
189 | | /* }}} */ |
190 | | /* {{{ php_stream_cast */ |
191 | | PHPAPI zend_result _php_stream_cast(php_stream *stream, int castas, void **ret, int show_err) |
192 | 0 | { |
193 | 0 | int flags = castas & PHP_STREAM_CAST_MASK; |
194 | 0 | castas &= ~PHP_STREAM_CAST_MASK; |
195 | | |
196 | | /* synchronize our buffer (if possible) */ |
197 | 0 | if (ret && castas != PHP_STREAM_AS_FD_FOR_SELECT && castas != PHP_STREAM_AS_FD_FOR_COPY) { |
198 | 0 | php_stream_flush(stream); |
199 | 0 | if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { |
200 | 0 | zend_off_t dummy; |
201 | |
|
202 | 0 | stream->ops->seek(stream, stream->position, SEEK_SET, &dummy); |
203 | 0 | stream->readpos = stream->writepos = 0; |
204 | 0 | } |
205 | 0 | } |
206 | |
|
207 | 0 | if (castas == PHP_STREAM_AS_FD_FOR_COPY) { |
208 | 0 | if (php_stream_is_filtered(stream)) { |
209 | 0 | return FAILURE; |
210 | 0 | } |
211 | 0 | if (stream->ops->cast && stream->ops->cast(stream, castas, ret) == SUCCESS) { |
212 | 0 | return SUCCESS; |
213 | 0 | } |
214 | 0 | return FAILURE; |
215 | 0 | } |
216 | | |
217 | | /* filtered streams can only be cast as stdio, and only when fopencookie is present */ |
218 | | |
219 | 0 | if (castas == PHP_STREAM_AS_STDIO) { |
220 | 0 | if (stream->stdiocast) { |
221 | 0 | if (ret) { |
222 | 0 | *(FILE**)ret = stream->stdiocast; |
223 | 0 | } |
224 | 0 | goto exit_success; |
225 | 0 | } |
226 | | |
227 | | /* if the stream is a stdio stream let's give it a chance to respond |
228 | | * first, to avoid doubling up the layers of stdio with an fopencookie */ |
229 | 0 | if (php_stream_is(stream, PHP_STREAM_IS_STDIO) && |
230 | 0 | stream->ops->cast && |
231 | 0 | !php_stream_is_filtered(stream) && |
232 | 0 | stream->ops->cast(stream, castas, ret) == SUCCESS |
233 | 0 | ) { |
234 | 0 | goto exit_success; |
235 | 0 | } |
236 | | |
237 | 0 | #ifdef HAVE_FOPENCOOKIE |
238 | | /* if just checking, say yes we can be a FILE*, but don't actually create it yet */ |
239 | 0 | if (ret == NULL) { |
240 | 0 | goto exit_success; |
241 | 0 | } |
242 | | |
243 | 0 | { |
244 | 0 | char fixed_mode[5]; |
245 | 0 | php_stream_mode_sanitize_fdopen_fopencookie(stream, fixed_mode); |
246 | 0 | *(FILE**)ret = fopencookie(stream, fixed_mode, PHP_STREAM_COOKIE_FUNCTIONS); |
247 | 0 | } |
248 | |
|
249 | 0 | if (*ret != NULL) { |
250 | 0 | zend_off_t pos; |
251 | |
|
252 | 0 | stream->fclose_stdiocast = PHP_STREAM_FCLOSE_FOPENCOOKIE; |
253 | | |
254 | | /* If the stream position is not at the start, we need to force |
255 | | * the stdio layer to believe it's real location. */ |
256 | 0 | pos = php_stream_tell(stream); |
257 | 0 | if (pos > 0) { |
258 | 0 | zend_fseek(*ret, pos, SEEK_SET); |
259 | 0 | } |
260 | |
|
261 | 0 | goto exit_success; |
262 | 0 | } |
263 | | |
264 | | /* must be either: |
265 | | a) programmer error |
266 | | b) no memory |
267 | | -> lets bail |
268 | | */ |
269 | 0 | php_stream_fatal(stream, CastFailed, "fopencookie failed"); |
270 | 0 | return FAILURE; |
271 | 0 | #endif |
272 | | |
273 | 0 | if (!php_stream_is_filtered(stream) && stream->ops->cast && stream->ops->cast(stream, castas, NULL) == SUCCESS) { |
274 | 0 | if (FAILURE == stream->ops->cast(stream, castas, ret)) { |
275 | 0 | return FAILURE; |
276 | 0 | } |
277 | 0 | goto exit_success; |
278 | 0 | } else if (flags & PHP_STREAM_CAST_TRY_HARD) { |
279 | 0 | php_stream *newstream; |
280 | |
|
281 | 0 | newstream = php_stream_fopen_tmpfile(); |
282 | 0 | if (newstream) { |
283 | 0 | zend_result retcopy = php_stream_copy_to_stream_ex(stream, newstream, PHP_STREAM_COPY_ALL, NULL); |
284 | |
|
285 | 0 | if (retcopy != SUCCESS) { |
286 | 0 | php_stream_close(newstream); |
287 | 0 | } else { |
288 | 0 | zend_result retcast = php_stream_cast(newstream, castas | flags, (void **)ret, show_err); |
289 | |
|
290 | 0 | if (retcast == SUCCESS) { |
291 | 0 | rewind(*(FILE**)ret); |
292 | 0 | } |
293 | | |
294 | | /* do some specialized cleanup */ |
295 | 0 | if ((flags & PHP_STREAM_CAST_RELEASE)) { |
296 | 0 | php_stream_free(stream, PHP_STREAM_FREE_CLOSE_CASTED); |
297 | 0 | } |
298 | | |
299 | | /* TODO: we probably should be setting .stdiocast and .fclose_stdiocast or |
300 | | * we may be leaking the FILE*. Needs investigation, though. */ |
301 | 0 | return retcast; |
302 | 0 | } |
303 | 0 | } |
304 | 0 | } |
305 | 0 | } |
306 | | |
307 | 0 | if (php_stream_is_filtered(stream) && castas != PHP_STREAM_AS_FD_FOR_SELECT) { |
308 | 0 | if (show_err) { |
309 | 0 | php_stream_warn(stream, CastNotSupported, |
310 | 0 | "Cannot cast a filtered stream on this system"); |
311 | 0 | } |
312 | 0 | return FAILURE; |
313 | 0 | } else if (stream->ops->cast && stream->ops->cast(stream, castas, ret) == SUCCESS) { |
314 | 0 | goto exit_success; |
315 | 0 | } |
316 | | |
317 | 0 | if (show_err) { |
318 | | /* these names depend on the values of the PHP_STREAM_AS_XXX defines in php_streams.h */ |
319 | 0 | static const char *cast_names[4] = { |
320 | 0 | "STDIO FILE*", |
321 | 0 | "File Descriptor", |
322 | 0 | "Socket Descriptor", |
323 | 0 | "select()able descriptor" |
324 | 0 | }; |
325 | |
|
326 | 0 | php_stream_warn(stream, CastNotSupported, |
327 | 0 | "Cannot represent a stream of type %s as a %s", stream->ops->label, cast_names[castas]); |
328 | 0 | } |
329 | |
|
330 | 0 | return FAILURE; |
331 | | |
332 | 0 | exit_success: |
333 | |
|
334 | 0 | if ((stream->writepos - stream->readpos) > 0 && |
335 | 0 | stream->fclose_stdiocast != PHP_STREAM_FCLOSE_FOPENCOOKIE && |
336 | 0 | (flags & PHP_STREAM_CAST_INTERNAL) == 0 |
337 | 0 | ) { |
338 | | /* the data we have buffered will be lost to the third party library that |
339 | | * will be accessing the stream. Emit a warning so that the end-user will |
340 | | * know that they should try something else */ |
341 | |
|
342 | 0 | php_stream_warn_nt(stream, BufferedDataLost, |
343 | 0 | ZEND_LONG_FMT " bytes of buffered data lost during stream conversion!", |
344 | 0 | (zend_long)(stream->writepos - stream->readpos)); |
345 | 0 | } |
346 | |
|
347 | 0 | if (castas == PHP_STREAM_AS_STDIO && ret) { |
348 | 0 | stream->stdiocast = *(FILE**)ret; |
349 | 0 | } |
350 | |
|
351 | 0 | if (flags & PHP_STREAM_CAST_RELEASE) { |
352 | 0 | php_stream_free(stream, PHP_STREAM_FREE_CLOSE_CASTED); |
353 | 0 | } |
354 | |
|
355 | 0 | return SUCCESS; |
356 | |
|
357 | 0 | } |
358 | | /* }}} */ |
359 | | |
360 | | /* {{{ php_stream_open_wrapper_as_file */ |
361 | | PHPAPI FILE * _php_stream_open_wrapper_as_file(char *path, char *mode, int options, zend_string **opened_path STREAMS_DC) |
362 | 0 | { |
363 | 0 | FILE *fp = NULL; |
364 | 0 | php_stream *stream = NULL; |
365 | |
|
366 | 0 | stream = php_stream_open_wrapper_rel(path, mode, options|STREAM_WILL_CAST, opened_path); |
367 | |
|
368 | 0 | if (stream == NULL) { |
369 | 0 | return NULL; |
370 | 0 | } |
371 | | |
372 | 0 | if (php_stream_cast(stream, PHP_STREAM_AS_STDIO|PHP_STREAM_CAST_TRY_HARD|PHP_STREAM_CAST_RELEASE, (void**)&fp, REPORT_ERRORS) == FAILURE) { |
373 | 0 | php_stream_close(stream); |
374 | 0 | if (opened_path && *opened_path) { |
375 | 0 | zend_string_release_ex(*opened_path, 0); |
376 | 0 | } |
377 | 0 | return NULL; |
378 | 0 | } |
379 | 0 | return fp; |
380 | 0 | } |
381 | | /* }}} */ |
382 | | |
383 | | /* {{{ php_stream_make_seekable */ |
384 | | PHPAPI php_stream_make_seekable_status _php_stream_make_seekable(php_stream *origstream, php_stream **newstream, int flags STREAMS_DC) |
385 | 0 | { |
386 | 0 | if (newstream == NULL) { |
387 | 0 | return PHP_STREAM_FAILED; |
388 | 0 | } |
389 | 0 | *newstream = NULL; |
390 | |
|
391 | 0 | if (((flags & PHP_STREAM_FORCE_CONVERSION) == 0) && origstream->ops->seek != NULL) { |
392 | 0 | *newstream = origstream; |
393 | 0 | return PHP_STREAM_UNCHANGED; |
394 | 0 | } |
395 | | |
396 | | /* Use a tmpfile and copy the old streams contents into it */ |
397 | | |
398 | 0 | if (flags & PHP_STREAM_PREFER_STDIO) { |
399 | 0 | *newstream = php_stream_fopen_tmpfile(); |
400 | 0 | } else { |
401 | 0 | *newstream = php_stream_temp_new(); |
402 | 0 | } |
403 | |
|
404 | 0 | if (*newstream == NULL) { |
405 | 0 | return PHP_STREAM_FAILED; |
406 | 0 | } |
407 | | |
408 | 0 | #if ZEND_DEBUG |
409 | 0 | (*newstream)->open_filename = origstream->open_filename; |
410 | 0 | (*newstream)->open_lineno = origstream->open_lineno; |
411 | 0 | #endif |
412 | |
|
413 | 0 | if (php_stream_copy_to_stream_ex(origstream, *newstream, PHP_STREAM_COPY_ALL, NULL) != SUCCESS) { |
414 | 0 | php_stream_close(*newstream); |
415 | 0 | *newstream = NULL; |
416 | 0 | return PHP_STREAM_CRITICAL; |
417 | 0 | } |
418 | | |
419 | 0 | php_stream_close(origstream); |
420 | 0 | php_stream_seek(*newstream, 0, SEEK_SET); |
421 | |
|
422 | 0 | return PHP_STREAM_RELEASED; |
423 | 0 | } |
424 | | /* }}} */ |