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_DIRSTREAM 1
21 #include "phar_internal.h"
22 #include "dirstream.h"
23
24 void phar_dostat(phar_archive_data *phar, phar_entry_info *data, php_stream_statbuf *ssb, bool is_dir);
25
26 static const php_stream_ops phar_dir_ops = {
27 phar_dir_write, /* write */
28 phar_dir_read, /* read */
29 phar_dir_close, /* close */
30 phar_dir_flush, /* flush */
31 "phar dir",
32 phar_dir_seek, /* seek */
33 NULL, /* cast */
34 NULL, /* stat */
35 NULL, /* set option */
36 };
37
38 /**
39 * Used for closedir($fp) where $fp is an opendir('phar://...') directory handle
40 */
phar_dir_close(php_stream * stream,int close_handle)41 static int phar_dir_close(php_stream *stream, int close_handle) /* {{{ */
42 {
43 HashTable *data = (HashTable *)stream->abstract;
44
45 if (data) {
46 zend_hash_destroy(data);
47 FREE_HASHTABLE(data);
48 stream->abstract = NULL;
49 }
50
51 return 0;
52 }
53 /* }}} */
54
55 /**
56 * Used for seeking on a phar directory handle
57 */
phar_dir_seek(php_stream * stream,zend_off_t offset,int whence,zend_off_t * newoffset)58 static int phar_dir_seek(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset) /* {{{ */
59 {
60 HashTable *data = (HashTable *)stream->abstract;
61
62 if (!data) {
63 return -1;
64 }
65
66 if (whence == SEEK_END) {
67 whence = SEEK_SET;
68 offset = zend_hash_num_elements(data) + offset;
69 }
70
71 if (whence == SEEK_SET) {
72 zend_hash_internal_pointer_reset(data);
73 }
74
75 if (offset < 0) {
76 return -1;
77 } else {
78 *newoffset = 0;
79 while (*newoffset < offset && zend_hash_move_forward(data) == SUCCESS) {
80 ++(*newoffset);
81 }
82 return 0;
83 }
84 }
85 /* }}} */
86
87 /**
88 * Used for readdir() on an opendir()ed phar directory handle
89 */
phar_dir_read(php_stream * stream,char * buf,size_t count)90 static ssize_t phar_dir_read(php_stream *stream, char *buf, size_t count) /* {{{ */
91 {
92 HashTable *data = (HashTable *)stream->abstract;
93 zend_string *str_key;
94 zend_ulong unused;
95
96 if (count != sizeof(php_stream_dirent)) {
97 return -1;
98 }
99
100 if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key(data, &str_key, &unused)) {
101 return 0;
102 }
103
104 zend_hash_move_forward(data);
105
106 php_stream_dirent *dirent = (php_stream_dirent *) buf;
107
108 if (sizeof(dirent->d_name) <= ZSTR_LEN(str_key)) {
109 return 0;
110 }
111
112 memset(dirent, 0, sizeof(php_stream_dirent));
113 PHP_STRLCPY(dirent->d_name, ZSTR_VAL(str_key), sizeof(dirent->d_name), ZSTR_LEN(str_key));
114
115 return sizeof(php_stream_dirent);
116 }
117 /* }}} */
118
119 /**
120 * Dummy: Used for writing to a phar directory (i.e. not used)
121 */
phar_dir_write(php_stream * stream,const char * buf,size_t count)122 static ssize_t phar_dir_write(php_stream *stream, const char *buf, size_t count) /* {{{ */
123 {
124 return -1;
125 }
126 /* }}} */
127
128 /**
129 * Dummy: Used for flushing writes to a phar directory (i.e. not used)
130 */
phar_dir_flush(php_stream * stream)131 static int phar_dir_flush(php_stream *stream) /* {{{ */
132 {
133 return EOF;
134 }
135 /* }}} */
136
137 /**
138 * Used for sorting directories alphabetically
139 */
phar_compare_dir_name(Bucket * f,Bucket * s)140 static int phar_compare_dir_name(Bucket *f, Bucket *s) /* {{{ */
141 {
142 int result = zend_binary_strcmp(
143 ZSTR_VAL(f->key), ZSTR_LEN(f->key), ZSTR_VAL(s->key), ZSTR_LEN(s->key));
144 return ZEND_NORMALIZE_BOOL(result);
145 }
146 /* }}} */
147
148 /**
149 * Create a opendir() directory stream handle by iterating over each of the
150 * files in a phar and retrieving its relative path. From this, construct
151 * a list of files/directories that are "in" the directory represented by dir
152 */
phar_make_dirstream(const char * dir,size_t dirlen,const HashTable * manifest)153 static php_stream *phar_make_dirstream(const char *dir, size_t dirlen, const HashTable *manifest) /* {{{ */
154 {
155 HashTable *data;
156 char *entry;
157
158 ALLOC_HASHTABLE(data);
159 zend_hash_init(data, 64, NULL, NULL, 0);
160
161 if ((*dir == '/' && dirlen == 1 && (manifest->nNumOfElements == 0)) || (dirlen >= sizeof(".phar")-1 && !memcmp(dir, ".phar", sizeof(".phar")-1))) {
162 /* make empty root directory for empty phar */
163 /* make empty directory for .phar magic directory */
164 return php_stream_alloc(&phar_dir_ops, data, NULL, "r");
165 }
166
167 zend_string *str_key;
168 ZEND_HASH_MAP_FOREACH_STR_KEY(manifest, str_key) {
169 size_t keylen = ZSTR_LEN(str_key);
170 if (keylen <= dirlen) {
171 if (keylen == 0 || keylen < dirlen || !strncmp(ZSTR_VAL(str_key), dir, dirlen)) {
172 continue;
173 }
174 }
175
176 if (*dir == '/') {
177 /* root directory */
178 if (zend_string_starts_with_literal(str_key, ".phar")) {
179 /* do not add any magic entries to this directory */
180 continue;
181 }
182
183 const char *has_slash = memchr(ZSTR_VAL(str_key), '/', keylen);
184 if (has_slash) {
185 /* the entry has a path separator and is a subdirectory */
186 keylen = has_slash - ZSTR_VAL(str_key);
187 }
188 entry = safe_emalloc(keylen, 1, 1);
189 memcpy(entry, ZSTR_VAL(str_key), keylen);
190 entry[keylen] = '\0';
191
192 goto PHAR_ADD_ENTRY;
193 } else {
194 if (0 != memcmp(ZSTR_VAL(str_key), dir, dirlen)) {
195 /* entry in directory not found */
196 continue;
197 } else {
198 if (ZSTR_VAL(str_key)[dirlen] != '/') {
199 continue;
200 }
201 }
202 }
203
204 const char *save = ZSTR_VAL(str_key);
205 save += dirlen + 1; /* seek to just past the path separator */
206
207 const char *has_slash = memchr(save, '/', keylen - dirlen - 1);
208 if (has_slash) {
209 /* is subdirectory */
210 save -= dirlen + 1;
211 entry = safe_emalloc(has_slash - save + dirlen, 1, 1);
212 memcpy(entry, save + dirlen + 1, has_slash - save - dirlen - 1);
213 keylen = has_slash - save - dirlen - 1;
214 entry[keylen] = '\0';
215 } else {
216 /* is file */
217 save -= dirlen + 1;
218 entry = safe_emalloc(keylen - dirlen, 1, 1);
219 memcpy(entry, save + dirlen + 1, keylen - dirlen - 1);
220 entry[keylen - dirlen - 1] = '\0';
221 keylen = keylen - dirlen - 1;
222 }
223 PHAR_ADD_ENTRY:
224 if (keylen) {
225 /**
226 * Add an empty element to avoid duplicates
227 *
228 * This is used to get a unique listing of virtual directories within a phar,
229 * for iterating over opendir()ed phar directories.
230 */
231 zval dummy;
232
233 ZVAL_NULL(&dummy);
234 zend_hash_str_update(data, entry, keylen, &dummy);
235 }
236
237 efree(entry);
238 } ZEND_HASH_FOREACH_END();
239
240 if (FAILURE != zend_hash_has_more_elements(data)) {
241 zend_hash_sort(data, phar_compare_dir_name, 0);
242 return php_stream_alloc(&phar_dir_ops, data, NULL, "r");
243 } else {
244 return php_stream_alloc(&phar_dir_ops, data, NULL, "r");
245 }
246 }
247 /* }}}*/
248
249 /**
250 * Open a directory handle within a phar archive
251 */
phar_wrapper_open_dir(php_stream_wrapper * wrapper,const char * path,const char * mode,int options,zend_string ** opened_path,php_stream_context * context STREAMS_DC)252 php_stream *phar_wrapper_open_dir(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC) /* {{{ */
253 {
254 php_url *resource = NULL;
255 char *error;
256 phar_archive_data *phar;
257
258 if ((resource = phar_parse_url(wrapper, path, mode, options)) == NULL) {
259 php_stream_wrapper_log_error(wrapper, options, "phar url \"%s\" is unknown", path);
260 return NULL;
261 }
262
263 /* we must have at the very least phar://alias.phar/ */
264 if (!resource->scheme || !resource->host || !resource->path) {
265 if (resource->host && !resource->path) {
266 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)", path, ZSTR_VAL(resource->host));
267 php_url_free(resource);
268 return NULL;
269 }
270 php_url_free(resource);
271 php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url \"%s\", must have at least phar://%s/", path, path);
272 return NULL;
273 }
274
275 if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
276 php_url_free(resource);
277 php_stream_wrapper_log_error(wrapper, options, "phar error: not a phar url \"%s\"", path);
278 return NULL;
279 }
280
281 phar_request_initialize();
282
283 if (FAILURE == phar_get_archive(&phar, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, &error)) {
284 if (error) {
285 php_stream_wrapper_log_error(wrapper, options, "%s", error);
286 efree(error);
287 } else {
288 php_stream_wrapper_log_error(wrapper, options, "phar file \"%s\" is unknown", ZSTR_VAL(resource->host));
289 }
290 php_url_free(resource);
291 return NULL;
292 }
293
294 if (error) {
295 efree(error);
296 }
297
298 if (zend_string_equals(resource->path, ZSTR_CHAR('/'))) {
299 /* root directory requested */
300 php_url_free(resource);
301 return phar_make_dirstream("/", strlen("/"), &phar->manifest);
302 }
303
304 if (!HT_IS_INITIALIZED(&phar->manifest)) {
305 php_url_free(resource);
306 return NULL;
307 }
308
309 const char *internal_file = ZSTR_VAL(resource->path) + 1; /* strip leading "/" */
310 size_t internal_file_len = ZSTR_LEN(resource->path) - 1;
311 phar_entry_info *entry = zend_hash_str_find_ptr(&phar->manifest, internal_file, internal_file_len);
312 php_stream *ret;
313
314 if (NULL != entry && !entry->is_dir) {
315 php_url_free(resource);
316 return NULL;
317 } else if (entry && entry->is_dir) {
318 if (entry->is_mounted) {
319 ret = php_stream_opendir(entry->tmp, options, context);
320 php_url_free(resource);
321 return ret;
322 }
323 ret = phar_make_dirstream(internal_file, internal_file_len, &phar->manifest);
324 php_url_free(resource);
325 return ret;
326 } else {
327 zend_string *str_key;
328
329 /* search for directory */
330 ZEND_HASH_MAP_FOREACH_STR_KEY(&phar->manifest, str_key) {
331 if (zend_string_starts_with_cstr(str_key, internal_file, internal_file_len)) {
332 /* directory found */
333 ret = phar_make_dirstream(internal_file, internal_file_len, &phar->manifest);
334 php_url_free(resource);
335 return ret;
336 }
337 } ZEND_HASH_FOREACH_END();
338 }
339
340 php_url_free(resource);
341 return NULL;
342 }
343 /* }}} */
344
345 /**
346 * Make a new directory within a phar archive
347 */
phar_wrapper_mkdir(php_stream_wrapper * wrapper,const char * url_from,int mode,int options,php_stream_context * context)348 int phar_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url_from, int mode, int options, php_stream_context *context) /* {{{ */
349 {
350 phar_entry_info entry, *e;
351 phar_archive_data *phar = NULL;
352 char *error, *arch, *entry2;
353 size_t arch_len, entry_len;
354 php_url *resource = NULL;
355
356 /* pre-readonly check, we need to know if this is a data phar */
357 if (FAILURE == phar_split_fname(url_from, strlen(url_from), &arch, &arch_len, &entry2, &entry_len, 2, 2)) {
358 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\", no phar archive specified", url_from);
359 return 0;
360 }
361
362 if (FAILURE == phar_get_archive(&phar, arch, arch_len, NULL, 0, NULL)) {
363 phar = NULL;
364 }
365
366 efree(arch);
367 efree(entry2);
368
369 if (PHAR_G(readonly) && (!phar || !phar->is_data)) {
370 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\", write operations disabled", url_from);
371 return 0;
372 }
373
374 if ((resource = phar_parse_url(wrapper, url_from, "w", options)) == NULL) {
375 return 0;
376 }
377
378 /* we must have at the very least phar://alias.phar/internalfile.php */
379 if (!resource->scheme || !resource->host || !resource->path) {
380 php_url_free(resource);
381 php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url \"%s\"", url_from);
382 return 0;
383 }
384
385 if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
386 php_url_free(resource);
387 php_stream_wrapper_log_error(wrapper, options, "phar error: not a phar stream url \"%s\"", url_from);
388 return 0;
389 }
390
391 if (FAILURE == phar_get_archive(&phar, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, &error)) {
392 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", error retrieving phar information: %s", ZSTR_VAL(resource->path) + 1, ZSTR_VAL(resource->host), error);
393 efree(error);
394 php_url_free(resource);
395 return 0;
396 }
397
398 if ((e = phar_get_entry_info_dir(phar, ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1, 2, &error, 1))) {
399 /* directory exists, or is a subdirectory of an existing file */
400 if (e->is_temp_dir) {
401 efree(e->filename);
402 efree(e);
403 }
404 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", directory already exists", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host));
405 php_url_free(resource);
406 return 0;
407 }
408
409 if (error) {
410 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", %s", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host), error);
411 efree(error);
412 php_url_free(resource);
413 return 0;
414 }
415
416 if (phar_get_entry_info_dir(phar, ZSTR_VAL(resource->path) + 1, ZSTR_LEN(resource->path) - 1, 0, &error, 1)) {
417 /* entry exists as a file */
418 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", file already exists", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host));
419 php_url_free(resource);
420 return 0;
421 }
422
423 if (error) {
424 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", %s", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host), error);
425 efree(error);
426 php_url_free(resource);
427 return 0;
428 }
429
430 memset((void *) &entry, 0, sizeof(phar_entry_info));
431
432 /* strip leading "/" */
433 if (phar->is_zip) {
434 entry.is_zip = 1;
435 }
436
437 entry.filename = estrdup(ZSTR_VAL(resource->path) + 1);
438
439 if (phar->is_tar) {
440 entry.is_tar = 1;
441 entry.tar_type = TAR_DIR;
442 }
443
444 entry.filename_len = ZSTR_LEN(resource->path) - 1;
445 php_url_free(resource);
446 entry.is_dir = 1;
447 entry.phar = phar;
448 entry.is_modified = 1;
449 entry.is_crc_checked = 1;
450 entry.flags = PHAR_ENT_PERM_DEF_DIR;
451 entry.old_flags = PHAR_ENT_PERM_DEF_DIR;
452
453 if (NULL == zend_hash_str_add_mem(&phar->manifest, entry.filename, entry.filename_len, (void*)&entry, sizeof(phar_entry_info))) {
454 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", adding to manifest failed", entry.filename, phar->fname);
455 efree(error);
456 efree(entry.filename);
457 return 0;
458 }
459
460 phar_flush(phar, &error);
461
462 if (error) {
463 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\" in phar \"%s\", %s", entry.filename, phar->fname, error);
464 zend_hash_str_del(&phar->manifest, entry.filename, entry.filename_len);
465 efree(error);
466 return 0;
467 }
468
469 phar_add_virtual_dirs(phar, entry.filename, entry.filename_len);
470 return 1;
471 }
472 /* }}} */
473
474 /**
475 * Remove a directory within a phar archive
476 */
phar_wrapper_rmdir(php_stream_wrapper * wrapper,const char * url,int options,php_stream_context * context)477 int phar_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context) /* {{{ */
478 {
479 phar_entry_info *entry;
480 phar_archive_data *phar = NULL;
481 char *error, *arch, *entry2;
482 size_t arch_len, entry_len;
483 php_url *resource = NULL;
484
485 /* pre-readonly check, we need to know if this is a data phar */
486 if (FAILURE == phar_split_fname(url, strlen(url), &arch, &arch_len, &entry2, &entry_len, 2, 2)) {
487 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\", no phar archive specified, or phar archive does not exist", url);
488 return 0;
489 }
490
491 if (FAILURE == phar_get_archive(&phar, arch, arch_len, NULL, 0, NULL)) {
492 phar = NULL;
493 }
494
495 efree(arch);
496 efree(entry2);
497
498 if (PHAR_G(readonly) && (!phar || !phar->is_data)) {
499 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot rmdir directory \"%s\", write operations disabled", url);
500 return 0;
501 }
502
503 if ((resource = phar_parse_url(wrapper, url, "w", options)) == NULL) {
504 return 0;
505 }
506
507 /* we must have at the very least phar://alias.phar/internalfile.php */
508 if (!resource->scheme || !resource->host || !resource->path) {
509 php_url_free(resource);
510 php_stream_wrapper_log_error(wrapper, options, "phar error: invalid url \"%s\"", url);
511 return 0;
512 }
513
514 if (!zend_string_equals_literal_ci(resource->scheme, "phar")) {
515 php_url_free(resource);
516 php_stream_wrapper_log_error(wrapper, options, "phar error: not a phar stream url \"%s\"", url);
517 return 0;
518 }
519
520 if (FAILURE == phar_get_archive(&phar, ZSTR_VAL(resource->host), ZSTR_LEN(resource->host), NULL, 0, &error)) {
521 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\" in phar \"%s\", error retrieving phar information: %s", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host), error);
522 efree(error);
523 php_url_free(resource);
524 return 0;
525 }
526
527 size_t path_len = ZSTR_LEN(resource->path) - 1;
528
529 if (!(entry = phar_get_entry_info_dir(phar, ZSTR_VAL(resource->path) + 1, path_len, 2, &error, 1))) {
530 if (error) {
531 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\" in phar \"%s\", %s", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host), error);
532 efree(error);
533 } else {
534 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\" in phar \"%s\", directory does not exist", ZSTR_VAL(resource->path)+1, ZSTR_VAL(resource->host));
535 }
536 php_url_free(resource);
537 return 0;
538 }
539
540 if (!entry->is_deleted) {
541 zend_string *str_key;
542
543 ZEND_HASH_MAP_FOREACH_STR_KEY(&phar->manifest, str_key) {
544 if (
545 zend_string_starts_with_cstr(str_key, ZSTR_VAL(resource->path)+1, path_len)
546 && IS_SLASH(ZSTR_VAL(str_key)[path_len])
547 ) {
548 php_stream_wrapper_log_error(wrapper, options, "phar error: Directory not empty");
549 if (entry->is_temp_dir) {
550 efree(entry->filename);
551 efree(entry);
552 }
553 php_url_free(resource);
554 return 0;
555 }
556 } ZEND_HASH_FOREACH_END();
557
558 ZEND_HASH_MAP_FOREACH_STR_KEY(&phar->virtual_dirs, str_key) {
559 ZEND_ASSERT(str_key);
560 if (
561 zend_string_starts_with_cstr(str_key, ZSTR_VAL(resource->path)+1, path_len)
562 && IS_SLASH(ZSTR_VAL(str_key)[path_len])
563 ) {
564 php_stream_wrapper_log_error(wrapper, options, "phar error: Directory not empty");
565 if (entry->is_temp_dir) {
566 efree(entry->filename);
567 efree(entry);
568 }
569 php_url_free(resource);
570 return 0;
571 }
572 } ZEND_HASH_FOREACH_END();
573 }
574
575 if (entry->is_temp_dir) {
576 zend_hash_str_del(&phar->virtual_dirs, ZSTR_VAL(resource->path)+1, path_len);
577 efree(entry->filename);
578 efree(entry);
579 } else {
580 entry->is_deleted = 1;
581 entry->is_modified = 1;
582 phar_flush(phar, &error);
583
584 if (error) {
585 php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\" in phar \"%s\", %s", entry->filename, phar->fname, error);
586 php_url_free(resource);
587 efree(error);
588 return 0;
589 }
590 }
591
592 php_url_free(resource);
593 return 1;
594 }
595 /* }}} */
596