/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) { |
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 | | /* filtered streams can only be cast as stdio, and only when fopencookie is present */ |
208 | |
|
209 | 0 | if (castas == PHP_STREAM_AS_STDIO) { |
210 | 0 | if (stream->stdiocast) { |
211 | 0 | if (ret) { |
212 | 0 | *(FILE**)ret = stream->stdiocast; |
213 | 0 | } |
214 | 0 | goto exit_success; |
215 | 0 | } |
216 | | |
217 | | /* if the stream is a stdio stream let's give it a chance to respond |
218 | | * first, to avoid doubling up the layers of stdio with an fopencookie */ |
219 | 0 | if (php_stream_is(stream, PHP_STREAM_IS_STDIO) && |
220 | 0 | stream->ops->cast && |
221 | 0 | !php_stream_is_filtered(stream) && |
222 | 0 | stream->ops->cast(stream, castas, ret) == SUCCESS |
223 | 0 | ) { |
224 | 0 | goto exit_success; |
225 | 0 | } |
226 | | |
227 | 0 | #ifdef HAVE_FOPENCOOKIE |
228 | | /* if just checking, say yes we can be a FILE*, but don't actually create it yet */ |
229 | 0 | if (ret == NULL) { |
230 | 0 | goto exit_success; |
231 | 0 | } |
232 | | |
233 | 0 | { |
234 | 0 | char fixed_mode[5]; |
235 | 0 | php_stream_mode_sanitize_fdopen_fopencookie(stream, fixed_mode); |
236 | 0 | *(FILE**)ret = fopencookie(stream, fixed_mode, PHP_STREAM_COOKIE_FUNCTIONS); |
237 | 0 | } |
238 | |
|
239 | 0 | if (*ret != NULL) { |
240 | 0 | zend_off_t pos; |
241 | |
|
242 | 0 | stream->fclose_stdiocast = PHP_STREAM_FCLOSE_FOPENCOOKIE; |
243 | | |
244 | | /* If the stream position is not at the start, we need to force |
245 | | * the stdio layer to believe it's real location. */ |
246 | 0 | pos = php_stream_tell(stream); |
247 | 0 | if (pos > 0) { |
248 | 0 | zend_fseek(*ret, pos, SEEK_SET); |
249 | 0 | } |
250 | |
|
251 | 0 | goto exit_success; |
252 | 0 | } |
253 | | |
254 | | /* must be either: |
255 | | a) programmer error |
256 | | b) no memory |
257 | | -> lets bail |
258 | | */ |
259 | 0 | php_stream_fatal(stream, CastFailed, "fopencookie failed"); |
260 | 0 | return FAILURE; |
261 | 0 | #endif |
262 | | |
263 | 0 | if (!php_stream_is_filtered(stream) && stream->ops->cast && stream->ops->cast(stream, castas, NULL) == SUCCESS) { |
264 | 0 | if (FAILURE == stream->ops->cast(stream, castas, ret)) { |
265 | 0 | return FAILURE; |
266 | 0 | } |
267 | 0 | goto exit_success; |
268 | 0 | } else if (flags & PHP_STREAM_CAST_TRY_HARD) { |
269 | 0 | php_stream *newstream; |
270 | |
|
271 | 0 | newstream = php_stream_fopen_tmpfile(); |
272 | 0 | if (newstream) { |
273 | 0 | zend_result retcopy = php_stream_copy_to_stream_ex(stream, newstream, PHP_STREAM_COPY_ALL, NULL); |
274 | |
|
275 | 0 | if (retcopy != SUCCESS) { |
276 | 0 | php_stream_close(newstream); |
277 | 0 | } else { |
278 | 0 | zend_result retcast = php_stream_cast(newstream, castas | flags, (void **)ret, show_err); |
279 | |
|
280 | 0 | if (retcast == SUCCESS) { |
281 | 0 | rewind(*(FILE**)ret); |
282 | 0 | } |
283 | | |
284 | | /* do some specialized cleanup */ |
285 | 0 | if ((flags & PHP_STREAM_CAST_RELEASE)) { |
286 | 0 | php_stream_free(stream, PHP_STREAM_FREE_CLOSE_CASTED); |
287 | 0 | } |
288 | | |
289 | | /* TODO: we probably should be setting .stdiocast and .fclose_stdiocast or |
290 | | * we may be leaking the FILE*. Needs investigation, though. */ |
291 | 0 | return retcast; |
292 | 0 | } |
293 | 0 | } |
294 | 0 | } |
295 | 0 | } |
296 | | |
297 | 0 | if (php_stream_is_filtered(stream) && castas != PHP_STREAM_AS_FD_FOR_SELECT) { |
298 | 0 | if (show_err) { |
299 | 0 | php_stream_warn(stream, CastNotSupported, |
300 | 0 | "Cannot cast a filtered stream on this system"); |
301 | 0 | } |
302 | 0 | return FAILURE; |
303 | 0 | } else if (stream->ops->cast && stream->ops->cast(stream, castas, ret) == SUCCESS) { |
304 | 0 | goto exit_success; |
305 | 0 | } |
306 | | |
307 | 0 | if (show_err) { |
308 | | /* these names depend on the values of the PHP_STREAM_AS_XXX defines in php_streams.h */ |
309 | 0 | static const char *cast_names[4] = { |
310 | 0 | "STDIO FILE*", |
311 | 0 | "File Descriptor", |
312 | 0 | "Socket Descriptor", |
313 | 0 | "select()able descriptor" |
314 | 0 | }; |
315 | |
|
316 | 0 | php_stream_warn(stream, CastNotSupported, |
317 | 0 | "Cannot represent a stream of type %s as a %s", stream->ops->label, cast_names[castas]); |
318 | 0 | } |
319 | |
|
320 | 0 | return FAILURE; |
321 | | |
322 | 0 | exit_success: |
323 | |
|
324 | 0 | if ((stream->writepos - stream->readpos) > 0 && |
325 | 0 | stream->fclose_stdiocast != PHP_STREAM_FCLOSE_FOPENCOOKIE && |
326 | 0 | (flags & PHP_STREAM_CAST_INTERNAL) == 0 |
327 | 0 | ) { |
328 | | /* the data we have buffered will be lost to the third party library that |
329 | | * will be accessing the stream. Emit a warning so that the end-user will |
330 | | * know that they should try something else */ |
331 | |
|
332 | 0 | php_stream_warn_nt(stream, BufferedDataLost, |
333 | 0 | ZEND_LONG_FMT " bytes of buffered data lost during stream conversion!", |
334 | 0 | (zend_long)(stream->writepos - stream->readpos)); |
335 | 0 | } |
336 | |
|
337 | 0 | if (castas == PHP_STREAM_AS_STDIO && ret) { |
338 | 0 | stream->stdiocast = *(FILE**)ret; |
339 | 0 | } |
340 | |
|
341 | 0 | if (flags & PHP_STREAM_CAST_RELEASE) { |
342 | 0 | php_stream_free(stream, PHP_STREAM_FREE_CLOSE_CASTED); |
343 | 0 | } |
344 | |
|
345 | 0 | return SUCCESS; |
346 | |
|
347 | 0 | } |
348 | | /* }}} */ |
349 | | |
350 | | /* {{{ php_stream_open_wrapper_as_file */ |
351 | | PHPAPI FILE * _php_stream_open_wrapper_as_file(char *path, char *mode, int options, zend_string **opened_path STREAMS_DC) |
352 | 0 | { |
353 | 0 | FILE *fp = NULL; |
354 | 0 | php_stream *stream = NULL; |
355 | |
|
356 | 0 | stream = php_stream_open_wrapper_rel(path, mode, options|STREAM_WILL_CAST, opened_path); |
357 | |
|
358 | 0 | if (stream == NULL) { |
359 | 0 | return NULL; |
360 | 0 | } |
361 | | |
362 | 0 | if (php_stream_cast(stream, PHP_STREAM_AS_STDIO|PHP_STREAM_CAST_TRY_HARD|PHP_STREAM_CAST_RELEASE, (void**)&fp, REPORT_ERRORS) == FAILURE) { |
363 | 0 | php_stream_close(stream); |
364 | 0 | if (opened_path && *opened_path) { |
365 | 0 | zend_string_release_ex(*opened_path, 0); |
366 | 0 | } |
367 | 0 | return NULL; |
368 | 0 | } |
369 | 0 | return fp; |
370 | 0 | } |
371 | | /* }}} */ |
372 | | |
373 | | /* {{{ php_stream_make_seekable */ |
374 | | PHPAPI php_stream_make_seekable_status _php_stream_make_seekable(php_stream *origstream, php_stream **newstream, int flags STREAMS_DC) |
375 | 0 | { |
376 | 0 | if (newstream == NULL) { |
377 | 0 | return PHP_STREAM_FAILED; |
378 | 0 | } |
379 | 0 | *newstream = NULL; |
380 | |
|
381 | 0 | if (((flags & PHP_STREAM_FORCE_CONVERSION) == 0) && origstream->ops->seek != NULL) { |
382 | 0 | *newstream = origstream; |
383 | 0 | return PHP_STREAM_UNCHANGED; |
384 | 0 | } |
385 | | |
386 | | /* Use a tmpfile and copy the old streams contents into it */ |
387 | | |
388 | 0 | if (flags & PHP_STREAM_PREFER_STDIO) { |
389 | 0 | *newstream = php_stream_fopen_tmpfile(); |
390 | 0 | } else { |
391 | 0 | *newstream = php_stream_temp_new(); |
392 | 0 | } |
393 | |
|
394 | 0 | if (*newstream == NULL) { |
395 | 0 | return PHP_STREAM_FAILED; |
396 | 0 | } |
397 | | |
398 | 0 | #if ZEND_DEBUG |
399 | 0 | (*newstream)->open_filename = origstream->open_filename; |
400 | 0 | (*newstream)->open_lineno = origstream->open_lineno; |
401 | 0 | #endif |
402 | |
|
403 | 0 | if (php_stream_copy_to_stream_ex(origstream, *newstream, PHP_STREAM_COPY_ALL, NULL) != SUCCESS) { |
404 | 0 | php_stream_close(*newstream); |
405 | 0 | *newstream = NULL; |
406 | 0 | return PHP_STREAM_CRITICAL; |
407 | 0 | } |
408 | | |
409 | 0 | php_stream_close(origstream); |
410 | 0 | php_stream_seek(*newstream, 0, SEEK_SET); |
411 | |
|
412 | 0 | return PHP_STREAM_RELEASED; |
413 | 0 | } |
414 | | /* }}} */ |