xref: /php-src/ext/phar/stream.c (revision 615960be)
1 /*
2   +----------------------------------------------------------------------+
3   | phar:// stream wrapper support                                       |
4   +----------------------------------------------------------------------+
5   | Copyright (c) The PHP Group                                          |
6   +----------------------------------------------------------------------+
7   | This source file is subject to version 3.01 of the PHP license,      |
8   | that is bundled with this package in the file LICENSE, and is        |
9   | available through the world-wide-web at the following url:           |
10   | https://www.php.net/license/3_01.txt                                 |
11   | If you did not receive a copy of the PHP license and are unable to   |
12   | obtain it through the world-wide-web, please send a note to          |
13   | license@php.net so we can mail you a copy immediately.               |
14   +----------------------------------------------------------------------+
15   | Authors: Gregory Beaver <cellog@php.net>                             |
16   |          Marcus Boerger <helly@php.net>                              |
17   +----------------------------------------------------------------------+
18 */
19 
20 #define PHAR_STREAM 1
21 #include "phar_internal.h"
22 #include "stream.h"
23 #include "dirstream.h"
24 
25 static const php_stream_ops phar_ops = {
26 	phar_stream_write, /* write */
27 	phar_stream_read,  /* read */
28 	phar_stream_close, /* close */
29 	phar_stream_flush, /* flush */
30 	"phar stream",
31 	phar_stream_seek,  /* seek */
32 	NULL,              /* cast */
33 	phar_stream_stat,  /* stat */
34 	NULL, /* set option */
35 };
36 
37 static const php_stream_wrapper_ops phar_stream_wops = {
38 	phar_wrapper_open_url,
39 	NULL,                  /* phar_wrapper_close */
40 	NULL,                  /* phar_wrapper_stat, */
41 	phar_wrapper_stat,     /* stat_url */
42 	phar_wrapper_open_dir, /* opendir */
43 	"phar",
44 	phar_wrapper_unlink,   /* unlink */
45 	phar_wrapper_rename,   /* rename */
46 	phar_wrapper_mkdir,    /* create directory */
47 	phar_wrapper_rmdir,    /* remove directory */
48 	NULL
49 };
50 
51 const php_stream_wrapper php_stream_phar_wrapper = {
52 	&phar_stream_wops,
53 	NULL,
54 	0 /* is_url */
55 };
56 
57 /**
58  * Open a phar file for streams API
59  */
phar_parse_url(php_stream_wrapper * wrapper,const char * filename,const char * mode,int options)60 php_url* phar_parse_url(php_stream_wrapper *wrapper, const char *filename, const char *mode, int options) /* {{{ */
61 {
62 	php_url *resource;
63 	char *arch = NULL, *entry = NULL, *error;
64 	size_t arch_len, entry_len;
65 
66 	if (strncasecmp(filename, "phar://", 7)) {
67 		return NULL;
68 	}
69 	if (mode[0] == 'a') {
70 		if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
71 			php_stream_wrapper_log_error(wrapper, options, "phar error: open mode append not supported");
72 		}
73 		return NULL;
74 	}
75 	if (phar_split_fname(filename, strlen(filename), &arch, &arch_len, &entry, &entry_len, 2, (mode[0] == 'w' ? 2 : 0)) == FAILURE) {
76 		if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
77 			if (arch && !entry) {
78 				php_stream_wrapper_log_error(wrapper, options, "phar error: no directory in \"%s\", must have at least phar://%s/ for root directory (always use full path to a new phar)", filename, arch);
79 				arch = NULL;
80 			} else {
81 				php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url or non-existent phar \"%s\"", filename);
82 			}
83 		}
84 		return NULL;
85 	}
86 	resource = ecalloc(1, sizeof(php_url));
87 	resource->scheme = ZSTR_INIT_LITERAL("phar", 0);
88 	resource->host = zend_string_init(arch, arch_len, 0);
89 	efree(arch);
90 	resource->path = zend_string_init(entry, entry_len, 0);
91 	efree(entry);
92 
93 #ifdef MBO_0
94 		if (resource) {
95 			fprintf(stderr, "Alias:     %s\n", alias);
96 			fprintf(stderr, "Scheme:    %s\n", ZSTR_VAL(resource->scheme));
97 /*			fprintf(stderr, "User:      %s\n", resource->user);*/
98 /*			fprintf(stderr, "Pass:      %s\n", resource->pass ? "***" : NULL);*/
99 			fprintf(stderr, "Host:      %s\n", ZSTR_VAL(resource->host));
100 /*			fprintf(stderr, "Port:      %d\n", resource->port);*/
101 			fprintf(stderr, "Path:      %s\n", ZSTR_VAL(resource->path));
102 /*			fprintf(stderr, "Query:     %s\n", resource->query);*/
103 /*			fprintf(stderr, "Fragment:  %s\n", resource->fragment);*/
104 		}
105 #endif
106 	if (mode[0] == 'w' || (mode[0] == 'r' && mode[1] == '+')) {
107 		phar_archive_data *pphar = NULL, *phar;
108 
109 		if (PHAR_G(request_init) && HT_IS_INITIALIZED(&PHAR_G(phar_fname_map)) && NULL == (pphar = zend_hash_find_ptr(&(PHAR_G(phar_fname_map)), resource->host))) {
110 			pphar = NULL;
111 		}
112 		if (PHAR_G(readonly) && (!pphar || !pphar->is_data)) {
113 			if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
114 				php_stream_wrapper_log_error(wrapper, options, "phar error: write operations disabled by the php.ini setting phar.readonly");
115 			}
116 			php_url_free(resource);
117 			return NULL;
118 		}
119 		if (phar_open_or_create_filename(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, 0, options, &phar, &error) == FAILURE)
120 		{
121 			if (error) {
122 				if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
123 					php_stream_wrapper_log_error(wrapper, options, "%s", error);
124 				}
125 				efree(error);
126 			}
127 			php_url_free(resource);
128 			return NULL;
129 		}
130 		if (phar->is_persistent && FAILURE == phar_copy_on_write(&phar)) {
131 			if (error) {
132 				spprintf(&error, 0, "Cannot open cached phar '%s' as writeable, copy on write failed", ZSTR_VAL(resource->host));
133 				if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
134 					php_stream_wrapper_log_error(wrapper, options, "%s", error);
135 				}
136 				efree(error);
137 			}
138 			php_url_free(resource);
139 			return NULL;
140 		}
141 	} else {
142 		if (phar_open_from_filename(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, options, NULL, &error) == FAILURE)
143 		{
144 			if (error) {
145 				if (!(options & PHP_STREAM_URL_STAT_QUIET)) {
146 					php_stream_wrapper_log_error(wrapper, options, "%s", error);
147 				}
148 				efree(error);
149 			}
150 			php_url_free(resource);
151 			return NULL;
152 		}
153 	}
154 	return resource;
155 }
156 /* }}} */
157 
158 /**
159  * used for fopen('phar://...') and company
160  */
phar_wrapper_open_url(php_stream_wrapper * wrapper,const char * path,const char * mode,int options,zend_string ** opened_path,php_stream_context * context STREAMS_DC)161 static php_stream * phar_wrapper_open_url(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
162 {
163 	phar_archive_data *phar;
164 	phar_entry_data *idata;
165 	char *internal_file;
166 	char *error;
167 	HashTable *pharcontext;
168 	php_url *resource = NULL;
169 	php_stream *fpf;
170 	zval *pzoption, *metadata;
171 
172 	if ((resource = phar_parse_url(wrapper, path, mode, options)) == NULL) {
173 		return NULL;
174 	}
175 
176 	/* we must have at the very least phar://alias.phar/internalfile.php */
177 	if (!resource->scheme || !resource->host || !resource->path) {
178 		php_url_free(resource);
179 		php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url \"%s\"", path);
180 		return NULL;
181 	}
182 
183 	if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
184 		php_url_free(resource);
185 		php_stream_wrapper_log_error(wrapper, options, "phar error: not a phar stream url \"%s\"", path);
186 		return NULL;
187 	}
188 
189 	phar_request_initialize();
190 
191 	/* strip leading "/" */
192 	internal_file = estrndup(ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1);
193 	if (mode[0] == 'w' || (mode[0] == 'r' && mode[1] == '+')) {
194 		if (NULL == (idata = phar_get_or_create_entry_data(ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), mode, 0, &error, 1))) {
195 			if (error) {
196 				php_stream_wrapper_log_error(wrapper, options, "%s", error);
197 				efree(error);
198 			} else {
199 				php_stream_wrapper_log_error(wrapper, options, "phar error: file \"%s\" could not be created in phar \"%s\"", internal_file, ZSTR_VAL(resource->host));
200 			}
201 			efree(internal_file);
202 			php_url_free(resource);
203 			return NULL;
204 		}
205 		if (error) {
206 			efree(error);
207 		}
208 		fpf = php_stream_alloc(&phar_ops, idata, NULL, mode);
209 		php_url_free(resource);
210 		efree(internal_file);
211 
212 		if (context && Z_TYPE(context->options) != IS_UNDEF && (pzoption = zend_hash_str_find(HASH_OF(&context->options), "phar", sizeof("phar")-1)) != NULL) {
213 			pharcontext = HASH_OF(pzoption);
214 			if (idata->internal_file->uncompressed_filesize == 0
215 				&& idata->internal_file->compressed_filesize == 0
216 				&& (pzoption = zend_hash_str_find(pharcontext, "compress", sizeof("compress")-1)) != NULL
217 				&& Z_TYPE_P(pzoption) == IS_LONG
218 				&& (Z_LVAL_P(pzoption) & ~PHAR_ENT_COMPRESSION_MASK) == 0
219 			) {
220 				idata->internal_file->flags &= ~PHAR_ENT_COMPRESSION_MASK;
221 				idata->internal_file->flags |= Z_LVAL_P(pzoption);
222 			}
223 			if ((pzoption = zend_hash_str_find(pharcontext, "metadata", sizeof("metadata")-1)) != NULL) {
224 				phar_metadata_tracker_free(&idata->internal_file->metadata_tracker, idata->internal_file->is_persistent);
225 
226 				metadata = pzoption;
227 				ZVAL_COPY_DEREF(&idata->internal_file->metadata_tracker.val, metadata);
228 				idata->phar->is_modified = 1;
229 			}
230 		}
231 		if (opened_path) {
232 			*opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->fname, idata->internal_file->filename);
233 		}
234 		return fpf;
235 	} else {
236 		if (!*internal_file && (options & STREAM_OPEN_FOR_INCLUDE)) {
237 			/* retrieve the stub */
238 			if (FAILURE == phar_get_archive(&phar, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, NULL)) {
239 				php_stream_wrapper_log_error(wrapper, options, "file %s is not a valid phar archive", ZSTR_VAL(resource->host));
240 				efree(internal_file);
241 				php_url_free(resource);
242 				return NULL;
243 			}
244 			if (phar->is_tar || phar->is_zip) {
245 				if ((FAILURE == phar_get_entry_data(&idata, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), ".phar/stub.php", sizeof(".phar/stub.php")-1, "r", 0, &error, 0)) || !idata) {
246 					goto idata_error;
247 				}
248 				efree(internal_file);
249 				if (opened_path) {
250 					*opened_path = strpprintf(MAXPATHLEN, "%s", phar->fname);
251 				}
252 				php_url_free(resource);
253 				goto phar_stub;
254 			} else {
255 				php_stream *stream = phar_get_pharfp(phar);
256 				if (stream == NULL) {
257 					if (UNEXPECTED(FAILURE == phar_open_archive_fp(phar))) {
258 						php_stream_wrapper_log_error(wrapper, options, "phar error: could not reopen phar \"%s\"", ZSTR_VAL(resource->host));
259 						efree(internal_file);
260 						php_url_free(resource);
261 						return NULL;
262 					}
263 					stream = phar_get_pharfp(phar);
264 				}
265 
266 				phar_entry_info *entry;
267 
268 				entry = (phar_entry_info *) ecalloc(1, sizeof(phar_entry_info));
269 				entry->is_temp_dir = 1;
270 				entry->filename = estrndup("", 0);
271 				entry->filename_len = 0;
272 				entry->phar = phar;
273 				entry->offset = entry->offset_abs = 0;
274 				entry->compressed_filesize = entry->uncompressed_filesize = phar->halt_offset;
275 				entry->is_crc_checked = 1;
276 
277 				idata = (phar_entry_data *) ecalloc(1, sizeof(phar_entry_data));
278 				idata->fp = stream;
279 				idata->phar = phar;
280 				idata->internal_file = entry;
281 				if (!phar->is_persistent) {
282 					++(entry->phar->refcount);
283 				}
284 				++(entry->fp_refcount);
285 				php_url_free(resource);
286 				if (opened_path) {
287 					*opened_path = strpprintf(MAXPATHLEN, "%s", phar->fname);
288 				}
289 				efree(internal_file);
290 				goto phar_stub;
291 			}
292 		}
293 		/* read-only access is allowed to magic files in .phar directory */
294 		if ((FAILURE == phar_get_entry_data(&idata, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, strlen(internal_file), "r", 0, &error, 0)) || !idata) {
295 idata_error:
296 			if (error) {
297 				php_stream_wrapper_log_error(wrapper, options, "%s", error);
298 				efree(error);
299 			} else {
300 				php_stream_wrapper_log_error(wrapper, options, "phar error: \"%s\" is not a file in phar \"%s\"", internal_file, ZSTR_VAL(resource->host));
301 			}
302 			efree(internal_file);
303 			php_url_free(resource);
304 			return NULL;
305 		}
306 	}
307 	php_url_free(resource);
308 #ifdef MBO_0
309 		fprintf(stderr, "Pharname:   %s\n", idata->phar->filename);
310 		fprintf(stderr, "Filename:   %s\n", internal_file);
311 		fprintf(stderr, "Entry:      %s\n", idata->internal_file->filename);
312 		fprintf(stderr, "Size:       %u\n", idata->internal_file->uncompressed_filesize);
313 		fprintf(stderr, "Compressed: %u\n", idata->internal_file->flags);
314 		fprintf(stderr, "Offset:     %u\n", idata->internal_file->offset_within_phar);
315 		fprintf(stderr, "Cached:     %s\n", idata->internal_file->filedata ? "yes" : "no");
316 #endif
317 
318 	/* check length, crc32 */
319 	if (!idata->internal_file->is_crc_checked && phar_postprocess_file(idata, idata->internal_file->crc32, &error, 2) != SUCCESS) {
320 		php_stream_wrapper_log_error(wrapper, options, "%s", error);
321 		efree(error);
322 		phar_entry_delref(idata);
323 		efree(internal_file);
324 		return NULL;
325 	}
326 
327 	if (!PHAR_G(cwd_init) && (options & STREAM_OPEN_FOR_INCLUDE)) {
328 		char *entry = idata->internal_file->filename, *cwd;
329 
330 		PHAR_G(cwd_init) = 1;
331 		if ((idata->phar->is_tar || idata->phar->is_zip) && idata->internal_file->filename_len == sizeof(".phar/stub.php")-1 && !strncmp(idata->internal_file->filename, ".phar/stub.php", sizeof(".phar/stub.php")-1)) {
332 			/* we're executing the stub, which doesn't count as a file */
333 			PHAR_G(cwd_init) = 0;
334 		} else if ((cwd = strrchr(entry, '/'))) {
335 			PHAR_G(cwd_len) = cwd - entry;
336 			PHAR_G(cwd) = estrndup(entry, PHAR_G(cwd_len));
337 		} else {
338 			/* root directory */
339 			PHAR_G(cwd_len) = 0;
340 			PHAR_G(cwd) = NULL;
341 		}
342 	}
343 	if (opened_path) {
344 		*opened_path = strpprintf(MAXPATHLEN, "phar://%s/%s", idata->phar->fname, idata->internal_file->filename);
345 	}
346 	efree(internal_file);
347 phar_stub:
348 	fpf = php_stream_alloc(&phar_ops, idata, NULL, mode);
349 	return fpf;
350 }
351 /* }}} */
352 
353 /**
354  * Used for fclose($fp) where $fp is a phar archive
355  */
phar_stream_close(php_stream * stream,int close_handle)356 static int phar_stream_close(php_stream *stream, int close_handle) /* {{{ */
357 {
358 	/* for some reasons phar needs to be flushed even if there is no write going on */
359 	phar_stream_flush(stream);
360 
361 	phar_entry_delref((phar_entry_data *)stream->abstract);
362 
363 	return 0;
364 }
365 /* }}} */
366 
367 /**
368  * used for fread($fp) and company on a fopen()ed phar file handle
369  */
phar_stream_read(php_stream * stream,char * buf,size_t count)370 static ssize_t phar_stream_read(php_stream *stream, char *buf, size_t count) /* {{{ */
371 {
372 	phar_entry_data *data = (phar_entry_data *)stream->abstract;
373 	ssize_t got;
374 	phar_entry_info *entry;
375 
376 	if (data->internal_file->link) {
377 		entry = phar_get_link_source(data->internal_file);
378 	} else {
379 		entry = data->internal_file;
380 	}
381 
382 	if (entry->is_deleted) {
383 		stream->eof = 1;
384 		return -1;
385 	}
386 
387 	/* use our proxy position */
388 	php_stream_seek(data->fp, data->position + data->zero, SEEK_SET);
389 
390 	got = php_stream_read(data->fp, buf, MIN(count, (size_t)(entry->uncompressed_filesize - data->position)));
391 	data->position = php_stream_tell(data->fp) - data->zero;
392 	stream->eof = (data->position == (zend_off_t) entry->uncompressed_filesize);
393 
394 	return got;
395 }
396 /* }}} */
397 
398 /**
399  * Used for fseek($fp) on a phar file handle
400  */
phar_stream_seek(php_stream * stream,zend_off_t offset,int whence,zend_off_t * newoffset)401 static int phar_stream_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset) /* {{{ */
402 {
403 	phar_entry_data *data = (phar_entry_data *)stream->abstract;
404 	phar_entry_info *entry;
405 	int res;
406 	zend_off_t temp;
407 
408 	if (data->internal_file->link) {
409 		entry = phar_get_link_source(data->internal_file);
410 	} else {
411 		entry = data->internal_file;
412 	}
413 
414 	switch (whence) {
415 		case SEEK_END :
416 			temp = data->zero + entry->uncompressed_filesize + offset;
417 			break;
418 		case SEEK_CUR :
419 			temp = data->zero + data->position + offset;
420 			break;
421 		case SEEK_SET :
422 			temp = data->zero + offset;
423 			break;
424 		default:
425 			temp = 0;
426 	}
427 	if (temp > data->zero + (zend_off_t) entry->uncompressed_filesize) {
428 		*newoffset = -1;
429 		return -1;
430 	}
431 	if (temp < data->zero) {
432 		*newoffset = -1;
433 		return -1;
434 	}
435 	res = php_stream_seek(data->fp, temp, SEEK_SET);
436 	*newoffset = php_stream_tell(data->fp) - data->zero;
437 	data->position = *newoffset;
438 	return res;
439 }
440 /* }}} */
441 
442 /**
443  * Used for writing to a phar file
444  */
phar_stream_write(php_stream * stream,const char * buf,size_t count)445 static ssize_t phar_stream_write(php_stream *stream, const char *buf, size_t count) /* {{{ */
446 {
447 	phar_entry_data *data = (phar_entry_data *) stream->abstract;
448 
449 	php_stream_seek(data->fp, data->position, SEEK_SET);
450 	if (count != php_stream_write(data->fp, buf, count)) {
451 		php_stream_wrapper_log_error(stream->wrapper, stream->flags, "phar error: Could not write %d characters to \"%s\" in phar \"%s\"", (int) count, data->internal_file->filename, data->phar->fname);
452 		return -1;
453 	}
454 	data->position = php_stream_tell(data->fp);
455 	if (data->position > (zend_off_t)data->internal_file->uncompressed_filesize) {
456 		data->internal_file->uncompressed_filesize = data->position;
457 	}
458 	data->internal_file->compressed_filesize = data->internal_file->uncompressed_filesize;
459 	data->internal_file->old_flags = data->internal_file->flags;
460 	data->internal_file->is_modified = 1;
461 	return count;
462 }
463 /* }}} */
464 
465 /**
466  * Used to save work done on a writeable phar
467  */
phar_stream_flush(php_stream * stream)468 static int phar_stream_flush(php_stream *stream) /* {{{ */
469 {
470 	char *error;
471 	phar_entry_data *data = (phar_entry_data *) stream->abstract;
472 
473 	if (data->internal_file->is_modified) {
474 		data->internal_file->timestamp = time(0);
475 		phar_flush(data->phar, &error);
476 		if (error) {
477 			php_stream_wrapper_log_error(stream->wrapper, REPORT_ERRORS, "%s", error);
478 			efree(error);
479 		}
480 		return EOF;
481 	} else {
482 		return EOF;
483 	}
484 }
485 /* }}} */
486 
487  /* {{{ phar_dostat */
488 /**
489  * stat an opened phar file handle stream, used by phar_stat()
490  */
phar_dostat(phar_archive_data * phar,phar_entry_info * data,php_stream_statbuf * ssb,bool is_temp_dir)491 void phar_dostat(phar_archive_data *phar, phar_entry_info *data, php_stream_statbuf *ssb, bool is_temp_dir)
492 {
493 	memset(ssb, 0, sizeof(php_stream_statbuf));
494 
495 	if (!is_temp_dir && !data->is_dir) {
496 		ssb->sb.st_size = data->uncompressed_filesize;
497 		ssb->sb.st_mode = data->flags & PHAR_ENT_PERM_MASK;
498 		ssb->sb.st_mode |= S_IFREG; /* regular file */
499 		/* timestamp is just the timestamp when this was added to the phar */
500 		ssb->sb.st_mtime = data->timestamp;
501 		ssb->sb.st_atime = data->timestamp;
502 		ssb->sb.st_ctime = data->timestamp;
503 	} else if (!is_temp_dir && data->is_dir) {
504 		ssb->sb.st_size = 0;
505 		ssb->sb.st_mode = data->flags & PHAR_ENT_PERM_MASK;
506 		ssb->sb.st_mode |= S_IFDIR; /* regular directory */
507 		/* timestamp is just the timestamp when this was added to the phar */
508 		ssb->sb.st_mtime = data->timestamp;
509 		ssb->sb.st_atime = data->timestamp;
510 		ssb->sb.st_ctime = data->timestamp;
511 	} else {
512 		ssb->sb.st_size = 0;
513 		ssb->sb.st_mode = 0777;
514 		ssb->sb.st_mode |= S_IFDIR; /* regular directory */
515 		ssb->sb.st_mtime = phar->max_timestamp;
516 		ssb->sb.st_atime = phar->max_timestamp;
517 		ssb->sb.st_ctime = phar->max_timestamp;
518 	}
519 	if (!phar->is_writeable) {
520 		ssb->sb.st_mode = (ssb->sb.st_mode & 0555) | (ssb->sb.st_mode & ~0777);
521 	}
522 
523 	ssb->sb.st_nlink = 1;
524 	ssb->sb.st_rdev = -1;
525 	/* this is only for APC, so use /dev/null device - no chance of conflict there! */
526 	ssb->sb.st_dev = 0xc;
527 	/* generate unique inode number for alias/filename, so no phars will conflict */
528 	if (!is_temp_dir) {
529 		ssb->sb.st_ino = data->inode;
530 	}
531 #ifndef PHP_WIN32
532 	ssb->sb.st_blksize = -1;
533 	ssb->sb.st_blocks = -1;
534 #endif
535 }
536 /* }}}*/
537 
538 /**
539  * Stat an opened phar file handle
540  */
phar_stream_stat(php_stream * stream,php_stream_statbuf * ssb)541 static int phar_stream_stat(php_stream *stream, php_stream_statbuf *ssb) /* {{{ */
542 {
543 	phar_entry_data *data = (phar_entry_data *)stream->abstract;
544 
545 	/* If ssb is NULL then someone is misbehaving */
546 	if (!ssb) {
547 		return -1;
548 	}
549 
550 	phar_dostat(data->phar, data->internal_file, ssb, 0);
551 	return 0;
552 }
553 /* }}} */
554 
555 /**
556  * Stream wrapper stat implementation of stat()
557  */
phar_wrapper_stat(php_stream_wrapper * wrapper,const char * url,int flags,php_stream_statbuf * ssb,php_stream_context * context)558 static int phar_wrapper_stat(php_stream_wrapper *wrapper, const char *url, int flags,
559 				  php_stream_statbuf *ssb, php_stream_context *context) /* {{{ */
560 {
561 	php_url *resource = NULL;
562 	char *internal_file, *error;
563 	phar_archive_data *phar;
564 	phar_entry_info *entry;
565 	size_t internal_file_len;
566 
567 	if ((resource = phar_parse_url(wrapper, url, "r", flags|PHP_STREAM_URL_STAT_QUIET)) == NULL) {
568 		return FAILURE;
569 	}
570 
571 	/* we must have at the very least phar://alias.phar/internalfile.php */
572 	if (!resource->scheme || !resource->host || !resource->path) {
573 		php_url_free(resource);
574 		return FAILURE;
575 	}
576 
577 	if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
578 		php_url_free(resource);
579 		return FAILURE;
580 	}
581 
582 	phar_request_initialize();
583 
584 	internal_file = ZSTR_VAL(resource->path) + 1; /* strip leading "/" */
585 	/* find the phar in our trusty global hash indexed by alias (host of phar://blah.phar/file.whatever) */
586 	if (FAILURE == phar_get_archive(&phar, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, &error)) {
587 		php_url_free(resource);
588 		if (error) {
589 			efree(error);
590 		}
591 		return FAILURE;
592 	}
593 	if (error) {
594 		efree(error);
595 	}
596 	if (*internal_file == '\0') {
597 		/* root directory requested */
598 		phar_dostat(phar, NULL, ssb, 1);
599 		php_url_free(resource);
600 		return SUCCESS;
601 	}
602 	if (!HT_IS_INITIALIZED(&phar->manifest)) {
603 		php_url_free(resource);
604 		return FAILURE;
605 	}
606 	internal_file_len = strlen(internal_file);
607 	/* search through the manifest of files, and if we have an exact match, it's a file */
608 	if (NULL != (entry = zend_hash_str_find_ptr(&phar->manifest, internal_file, internal_file_len))) {
609 		phar_dostat(phar, entry, ssb, 0);
610 		php_url_free(resource);
611 		return SUCCESS;
612 	}
613 	if (zend_hash_str_exists(&(phar->virtual_dirs), internal_file, internal_file_len)) {
614 		phar_dostat(phar, NULL, ssb, 1);
615 		php_url_free(resource);
616 		return SUCCESS;
617 	}
618 	/* check for mounted directories */
619 	if (HT_IS_INITIALIZED(&phar->mounted_dirs) && zend_hash_num_elements(&phar->mounted_dirs)) {
620 		zend_string *str_key;
621 
622 		ZEND_HASH_MAP_FOREACH_STR_KEY(&phar->mounted_dirs, str_key) {
623 			if (ZSTR_LEN(str_key) >= internal_file_len || strncmp(ZSTR_VAL(str_key), internal_file, ZSTR_LEN(str_key))) {
624 				continue;
625 			} else {
626 				char *test;
627 				size_t test_len;
628 				php_stream_statbuf ssbi;
629 
630 				if (NULL == (entry = zend_hash_find_ptr(&phar->manifest, str_key))) {
631 					goto free_resource;
632 				}
633 				if (!entry->tmp || !entry->is_mounted) {
634 					goto free_resource;
635 				}
636 				test_len = spprintf(&test, MAXPATHLEN, "%s%s", entry->tmp, internal_file + ZSTR_LEN(str_key));
637 				if (SUCCESS != php_stream_stat_path(test, &ssbi)) {
638 					efree(test);
639 					continue;
640 				}
641 				/* mount the file/directory just in time */
642 				if (SUCCESS != phar_mount_entry(phar, test, test_len, internal_file, internal_file_len)) {
643 					efree(test);
644 					goto free_resource;
645 				}
646 				efree(test);
647 				if (NULL == (entry = zend_hash_str_find_ptr(&phar->manifest, internal_file, internal_file_len))) {
648 					goto free_resource;
649 				}
650 				phar_dostat(phar, entry, ssb, 0);
651 				php_url_free(resource);
652 				return SUCCESS;
653 			}
654 		} ZEND_HASH_FOREACH_END();
655 	}
656 free_resource:
657 	php_url_free(resource);
658 	return FAILURE;
659 }
660 /* }}} */
661 
662 /**
663  * Unlink a file within a phar archive
664  */
phar_wrapper_unlink(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)665 static int phar_wrapper_unlink(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context) /* {{{ */
666 {
667 	php_url *resource;
668 	char *internal_file, *error;
669 	int internal_file_len;
670 	phar_entry_data *idata;
671 	phar_archive_data *pphar;
672 
673 	if ((resource = phar_parse_url(wrapper, url, "rb", options)) == NULL) {
674 		php_stream_wrapper_log_error(wrapper, options, "phar error: unlink failed");
675 		return 0;
676 	}
677 
678 	/* we must have at the very least phar://alias.phar/internalfile.php */
679 	if (!resource->scheme || !resource->host || !resource->path) {
680 		php_url_free(resource);
681 		php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url \"%s\"", url);
682 		return 0;
683 	}
684 
685 	if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
686 		php_url_free(resource);
687 		php_stream_wrapper_log_error(wrapper, options, "phar error: not a phar stream url \"%s\"", url);
688 		return 0;
689 	}
690 
691 	phar_request_initialize();
692 
693 	pphar = zend_hash_find_ptr(&(PHAR_G(phar_fname_map)), resource->host);
694 	if (PHAR_G(readonly) && (!pphar || !pphar->is_data)) {
695 		php_url_free(resource);
696 		php_stream_wrapper_log_error(wrapper, options, "phar error: write operations disabled by the php.ini setting phar.readonly");
697 		return 0;
698 	}
699 
700 	/* need to copy to strip leading "/", will get touched again */
701 	internal_file = estrndup(ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1);
702 	internal_file_len = ZSTR_LEN(resource->path) - 1;
703 	if (FAILURE == phar_get_entry_data(&idata, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), internal_file, internal_file_len, "r", 0, &error, 1)) {
704 		/* constraints of fp refcount were not met */
705 		if (error) {
706 			php_stream_wrapper_log_error(wrapper, options, "unlink of \"%s\" failed: %s", url, error);
707 			efree(error);
708 		} else {
709 			php_stream_wrapper_log_error(wrapper, options, "unlink of \"%s\" failed, file does not exist", url);
710 		}
711 		efree(internal_file);
712 		php_url_free(resource);
713 		return 0;
714 	}
715 	if (error) {
716 		efree(error);
717 	}
718 	if (idata->internal_file->fp_refcount > 1) {
719 		/* more than just our fp resource is open for this file */
720 		php_stream_wrapper_log_error(wrapper, options, "phar error: \"%s\" in phar \"%s\", has open file pointers, cannot unlink", internal_file, ZSTR_VAL(resource->host));
721 		efree(internal_file);
722 		php_url_free(resource);
723 		phar_entry_delref(idata);
724 		return 0;
725 	}
726 	php_url_free(resource);
727 	efree(internal_file);
728 	phar_entry_remove(idata, &error);
729 	if (error) {
730 		php_stream_wrapper_log_error(wrapper, options, "%s", error);
731 		efree(error);
732 	}
733 	return 1;
734 }
735 /* }}} */
736 
phar_wrapper_rename(php_stream_wrapper * wrapper,const char * url_from,const char * url_to,int options,php_stream_context * context)737 static int phar_wrapper_rename(php_stream_wrapper *wrapper, const char *url_from, const char *url_to, int options, php_stream_context *context) /* {{{ */
738 {
739 	php_url *resource_from, *resource_to;
740 	char *error;
741 	phar_archive_data *phar, *pfrom, *pto;
742 	phar_entry_info *entry;
743 	int is_dir = 0;
744 	int is_modified = 0;
745 
746 	error = NULL;
747 
748 	if ((resource_from = phar_parse_url(wrapper, url_from, "wb", options|PHP_STREAM_URL_STAT_QUIET)) == NULL) {
749 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": invalid or non-writable url \"%s\"", url_from, url_to, url_from);
750 		return 0;
751 	}
752 	if (SUCCESS != phar_get_archive(&pfrom, ZSTR_VAL(resource_from->host), ZSTR_LEN(resource_from->host), NULL, 0, &error)) {
753 		pfrom = NULL;
754 		if (error) {
755 			efree(error);
756 		}
757 	}
758 	if (PHAR_G(readonly) && (!pfrom || !pfrom->is_data)) {
759 		php_url_free(resource_from);
760 		php_error_docref(NULL, E_WARNING, "phar error: Write operations disabled by the php.ini setting phar.readonly");
761 		return 0;
762 	}
763 
764 	if ((resource_to = phar_parse_url(wrapper, url_to, "wb", options|PHP_STREAM_URL_STAT_QUIET)) == NULL) {
765 		php_url_free(resource_from);
766 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": invalid or non-writable url \"%s\"", url_from, url_to, url_to);
767 		return 0;
768 	}
769 	if (SUCCESS != phar_get_archive(&pto, ZSTR_VAL(resource_to->host), ZSTR_LEN(resource_to->host), NULL, 0, &error)) {
770 		if (error) {
771 			efree(error);
772 		}
773 		pto = NULL;
774 	}
775 	if (PHAR_G(readonly) && (!pto || !pto->is_data)) {
776 		php_url_free(resource_from);
777 		php_url_free(resource_to);
778 		php_error_docref(NULL, E_WARNING, "phar error: Write operations disabled by the php.ini setting phar.readonly");
779 		return 0;
780 	}
781 
782 	if (!zend_string_equals(resource_from->host, resource_to->host)) {
783 		php_url_free(resource_from);
784 		php_url_free(resource_to);
785 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\", not within the same phar archive", url_from, url_to);
786 		return 0;
787 	}
788 
789 	/* we must have at the very least phar://alias.phar/internalfile.php */
790 	if (!resource_from->scheme || !resource_from->host || !resource_from->path) {
791 		php_url_free(resource_from);
792 		php_url_free(resource_to);
793 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": invalid url \"%s\"", url_from, url_to, url_from);
794 		return 0;
795 	}
796 
797 	if (!resource_to->scheme || !resource_to->host || !resource_to->path) {
798 		php_url_free(resource_from);
799 		php_url_free(resource_to);
800 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": invalid url \"%s\"", url_from, url_to, url_to);
801 		return 0;
802 	}
803 
804 	if (!zend_string_equals_literal_ci(resource_from->scheme, "phar")) {
805 		php_url_free(resource_from);
806 		php_url_free(resource_to);
807 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": not a phar stream url \"%s\"", url_from, url_to, url_from);
808 		return 0;
809 	}
810 
811 	if (!zend_string_equals_literal_ci(resource_to->scheme, "phar")) {
812 		php_url_free(resource_from);
813 		php_url_free(resource_to);
814 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": not a phar stream url \"%s\"", url_from, url_to, url_to);
815 		return 0;
816 	}
817 
818 	if (SUCCESS != phar_get_archive(&phar, ZSTR_VAL(resource_from->host), ZSTR_LEN(resource_from->host), NULL, 0, &error)) {
819 		php_url_free(resource_from);
820 		php_url_free(resource_to);
821 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": %s", url_from, url_to, error);
822 		efree(error);
823 		return 0;
824 	}
825 
826 	if (phar->is_persistent && FAILURE == phar_copy_on_write(&phar)) {
827 		php_url_free(resource_from);
828 		php_url_free(resource_to);
829 		php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": could not make cached phar writeable", url_from, url_to);
830 		return 0;
831 	}
832 
833 	if (NULL != (entry = zend_hash_str_find_ptr(&(phar->manifest), ZSTR_VAL(resource_from->path)+1, ZSTR_LEN(resource_from->path)-1))) {
834 		phar_entry_info new, *source;
835 
836 		/* perform rename magic */
837 		if (entry->is_deleted) {
838 			php_url_free(resource_from);
839 			php_url_free(resource_to);
840 			php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\" from extracted phar archive, source has been deleted", url_from, url_to);
841 			return 0;
842 		}
843 		/* transfer all data over to the new entry */
844 		memcpy((void *) &new, (void *) entry, sizeof(phar_entry_info));
845 		/* mark the old one for deletion */
846 		entry->is_deleted = 1;
847 		entry->fp = NULL;
848 		ZVAL_UNDEF(&entry->metadata_tracker.val);
849 		entry->link = entry->tmp = NULL;
850 		source = entry;
851 
852 		/* add to the manifest, and then store the pointer to the new guy in entry
853 		 * if it already exists, we overwrite the destination like what copy('phar://...', 'phar://...') does. */
854 		entry = zend_hash_str_update_mem(&(phar->manifest), ZSTR_VAL(resource_to->path)+1, ZSTR_LEN(resource_to->path)-1, (void **)&new, sizeof(phar_entry_info));
855 
856 		entry->filename = estrndup(ZSTR_VAL(resource_to->path)+1, ZSTR_LEN(resource_to->path)-1);
857 		if (FAILURE == phar_copy_entry_fp(source, entry, &error)) {
858 			php_url_free(resource_from);
859 			php_url_free(resource_to);
860 			php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": %s", url_from, url_to, error);
861 			efree(error);
862 			zend_hash_str_del(&(phar->manifest), entry->filename, strlen(entry->filename));
863 			return 0;
864 		}
865 		is_modified = 1;
866 		entry->is_modified = 1;
867 		entry->filename_len = strlen(entry->filename);
868 		is_dir = entry->is_dir;
869 	} else {
870 		is_dir = zend_hash_str_exists(&(phar->virtual_dirs), ZSTR_VAL(resource_from->path)+1, ZSTR_LEN(resource_from->path)-1);
871 		if (!is_dir) {
872 			/* file does not exist */
873 			php_url_free(resource_from);
874 			php_url_free(resource_to);
875 			php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\" from extracted phar archive, source does not exist", url_from, url_to);
876 			return 0;
877 
878 		}
879 	}
880 
881 	/* Rename directory. Update all nested paths */
882 	if (is_dir) {
883 		Bucket *b;
884 		zend_string *str_key;
885 		zend_string *new_str_key;
886 		size_t from_len = ZSTR_LEN(resource_from->path) - 1;
887 		size_t to_len = ZSTR_LEN(resource_to->path) - 1;
888 
889 		ZEND_HASH_MAP_FOREACH_BUCKET(&phar->manifest, b) {
890 			str_key = b->key;
891 			entry = Z_PTR(b->val);
892 			if (!entry->is_deleted &&
893 				ZSTR_LEN(str_key) > from_len &&
894 				memcmp(ZSTR_VAL(str_key), ZSTR_VAL(resource_from->path)+1, from_len) == 0 &&
895 				IS_SLASH(ZSTR_VAL(str_key)[from_len])) {
896 
897 				new_str_key = zend_string_alloc(ZSTR_LEN(str_key) + to_len - from_len, 0);
898 				memcpy(ZSTR_VAL(new_str_key), ZSTR_VAL(resource_to->path) + 1, to_len);
899 				memcpy(ZSTR_VAL(new_str_key) + to_len, ZSTR_VAL(str_key) + from_len, ZSTR_LEN(str_key) - from_len);
900 				ZSTR_VAL(new_str_key)[ZSTR_LEN(new_str_key)] = 0;
901 
902 				is_modified = 1;
903 				entry->is_modified = 1;
904 				efree(entry->filename);
905 				// TODO: avoid reallocation (make entry->filename zend_string*)
906 				entry->filename = estrndup(ZSTR_VAL(new_str_key), ZSTR_LEN(new_str_key));
907 				entry->filename_len = ZSTR_LEN(new_str_key);
908 
909 				zend_string_release_ex(str_key, 0);
910 				b->h = zend_string_hash_val(new_str_key);
911 				b->key = new_str_key;
912 			}
913 		} ZEND_HASH_FOREACH_END();
914 		zend_hash_rehash(&phar->manifest);
915 
916 		ZEND_HASH_MAP_FOREACH_BUCKET(&phar->virtual_dirs, b) {
917 			str_key = b->key;
918 			if (zend_string_starts_with_cstr(str_key, ZSTR_VAL(resource_from->path)+1, from_len) &&
919 				(ZSTR_LEN(str_key) == from_len || IS_SLASH(ZSTR_VAL(str_key)[from_len]))) {
920 
921 				new_str_key = zend_string_alloc(ZSTR_LEN(str_key) + to_len - from_len, 0);
922 				memcpy(ZSTR_VAL(new_str_key), ZSTR_VAL(resource_to->path) + 1, to_len);
923 				memcpy(ZSTR_VAL(new_str_key) + to_len, ZSTR_VAL(str_key) + from_len, ZSTR_LEN(str_key) - from_len);
924 				ZSTR_VAL(new_str_key)[ZSTR_LEN(new_str_key)] = 0;
925 
926 				zend_string_release_ex(str_key, 0);
927 				b->h = zend_string_hash_val(new_str_key);
928 				b->key = new_str_key;
929 			}
930 		} ZEND_HASH_FOREACH_END();
931 		zend_hash_rehash(&phar->virtual_dirs);
932 
933 		ZEND_HASH_MAP_FOREACH_BUCKET(&phar->mounted_dirs, b) {
934 			str_key = b->key;
935 			if (zend_string_starts_with_cstr(str_key, ZSTR_VAL(resource_from->path)+1, from_len) &&
936 				(ZSTR_LEN(str_key) == from_len || IS_SLASH(ZSTR_VAL(str_key)[from_len]))) {
937 
938 				new_str_key = zend_string_alloc(ZSTR_LEN(str_key) + to_len - from_len, 0);
939 				memcpy(ZSTR_VAL(new_str_key), ZSTR_VAL(resource_to->path) + 1, to_len);
940 				memcpy(ZSTR_VAL(new_str_key) + to_len, ZSTR_VAL(str_key) + from_len, ZSTR_LEN(str_key) - from_len);
941 				ZSTR_VAL(new_str_key)[ZSTR_LEN(new_str_key)] = 0;
942 
943 				zend_string_release_ex(str_key, 0);
944 				b->h = zend_string_hash_val(new_str_key);
945 				b->key = new_str_key;
946 			}
947 		} ZEND_HASH_FOREACH_END();
948 		zend_hash_rehash(&phar->mounted_dirs);
949 	}
950 
951 	if (is_modified) {
952 		phar_flush(phar, &error);
953 		if (error) {
954 			php_url_free(resource_from);
955 			php_url_free(resource_to);
956 			php_error_docref(NULL, E_WARNING, "phar error: cannot rename \"%s\" to \"%s\": %s", url_from, url_to, error);
957 			efree(error);
958 			return 0;
959 		}
960 	}
961 
962 	php_url_free(resource_from);
963 	php_url_free(resource_to);
964 
965 	return 1;
966 }
967 /* }}} */
968