Coverage Report

Created: 2025-09-27 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/standard/dir.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright (c) The PHP Group                                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to version 3.01 of the PHP license,      |
6
   | that is bundled with this package in the file LICENSE, and is        |
7
   | available through the world-wide-web at the following url:           |
8
   | https://www.php.net/license/3_01.txt                                 |
9
   | If you did not receive a copy of the PHP license and are unable to   |
10
   | obtain it through the world-wide-web, please send a note to          |
11
   | license@php.net so we can mail you a copy immediately.               |
12
   +----------------------------------------------------------------------+
13
   | Author: Thies C. Arntzen <thies@thieso.net>                          |
14
   +----------------------------------------------------------------------+
15
 */
16
17
/* {{{ includes/startup/misc */
18
19
#include "php.h"
20
#include "fopen_wrappers.h"
21
#include "file.h"
22
#include "php_dir.h"
23
#include "php_dir_int.h"
24
#include "php_scandir.h"
25
#include "basic_functions.h"
26
#include "dir_arginfo.h"
27
28
#ifdef HAVE_UNISTD_H
29
#include <unistd.h>
30
#endif
31
32
#include <errno.h>
33
34
#ifdef PHP_WIN32
35
#include "win32/readdir.h"
36
#endif
37
38
typedef struct {
39
  zend_resource *default_dir;
40
} php_dir_globals;
41
42
#ifdef ZTS
43
#define DIRG(v) ZEND_TSRMG(dir_globals_id, php_dir_globals *, v)
44
int dir_globals_id;
45
#else
46
279k
#define DIRG(v) (dir_globals.v)
47
php_dir_globals dir_globals;
48
#endif
49
50
static zend_class_entry *dir_class_entry_ptr;
51
static zend_object_handlers dir_class_object_handlers;
52
53
#define Z_DIRECTORY_PATH_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 0)
54
0
#define Z_DIRECTORY_HANDLE_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 1)
55
56
static zend_function *dir_class_get_constructor(zend_object *object)
57
5
{
58
5
  zend_throw_error(NULL, "Cannot directly construct Directory, use dir() instead");
59
5
  return NULL;
60
5
}
61
62
static void php_set_default_dir(zend_resource *res)
63
280
{
64
280
  if (DIRG(default_dir)) {
65
140
    zend_list_delete(DIRG(default_dir));
66
140
  }
67
68
280
  if (res) {
69
140
    GC_ADDREF(res);
70
140
  }
71
72
280
  DIRG(default_dir) = res;
73
280
}
74
75
PHP_RINIT_FUNCTION(dir)
76
278k
{
77
278k
  DIRG(default_dir) = NULL;
78
278k
  return SUCCESS;
79
278k
}
80
81
PHP_MINIT_FUNCTION(dir)
82
16
{
83
16
  dirsep_str[0] = DEFAULT_SLASH;
84
16
  dirsep_str[1] = '\0';
85
86
16
  pathsep_str[0] = ZEND_PATHS_SEPARATOR;
87
16
  pathsep_str[1] = '\0';
88
89
16
  register_dir_symbols(module_number);
90
91
16
  dir_class_entry_ptr = register_class_Directory();
92
16
  dir_class_entry_ptr->default_object_handlers = &dir_class_object_handlers;
93
94
16
  memcpy(&dir_class_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
95
16
  dir_class_object_handlers.get_constructor = dir_class_get_constructor;
96
16
  dir_class_object_handlers.clone_obj = NULL;
97
16
  dir_class_object_handlers.compare = zend_objects_not_comparable;
98
99
#ifdef ZTS
100
  ts_allocate_id(&dir_globals_id, sizeof(php_dir_globals), NULL, NULL);
101
#endif
102
103
16
  return SUCCESS;
104
16
}
105
/* }}} */
106
107
/* {{{ internal functions */
108
static void _php_do_opendir(INTERNAL_FUNCTION_PARAMETERS, int createobject)
109
153
{
110
153
  char *dirname;
111
153
  size_t dir_len;
112
153
  zval *zcontext = NULL;
113
153
  php_stream_context *context = NULL;
114
153
  php_stream *dirp;
115
116
459
  ZEND_PARSE_PARAMETERS_START(1, 2)
117
612
    Z_PARAM_PATH(dirname, dir_len)
118
152
    Z_PARAM_OPTIONAL
119
304
    Z_PARAM_RESOURCE_OR_NULL(zcontext)
120
153
  ZEND_PARSE_PARAMETERS_END();
121
122
152
  context = php_stream_context_from_zval(zcontext, 0);
123
124
152
  dirp = php_stream_opendir(dirname, REPORT_ERRORS, context);
125
126
152
  if (dirp == NULL) {
127
12
    RETURN_FALSE;
128
12
  }
129
130
140
  dirp->flags |= PHP_STREAM_FLAG_NO_FCLOSE;
131
132
140
  php_set_default_dir(dirp->res);
133
134
140
  if (createobject) {
135
0
    object_init_ex(return_value, dir_class_entry_ptr);
136
0
    ZVAL_STRINGL(Z_DIRECTORY_PATH_P(return_value), dirname, dir_len);
137
0
    ZVAL_RES(Z_DIRECTORY_HANDLE_P(return_value), dirp->res);
138
0
    php_stream_auto_cleanup(dirp); /* so we don't get warnings under debug */
139
140
  } else {
140
140
    php_stream_to_zval(dirp, return_value);
141
140
  }
142
140
}
143
/* }}} */
144
145
/* {{{ Open a directory and return a dir_handle */
146
PHP_FUNCTION(opendir)
147
147
{
148
147
  _php_do_opendir(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
149
147
}
150
/* }}} */
151
152
/* {{{ Directory class with properties, handle and class and methods read, rewind and close */
153
PHP_FUNCTION(dir)
154
6
{
155
6
  _php_do_opendir(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
156
6
}
157
/* }}} */
158
159
160
static php_stream* php_dir_get_directory_stream_from_user_arg(php_stream *dir_stream)
161
140
{
162
140
  if (dir_stream == NULL) {
163
0
    php_error_docref(NULL, E_DEPRECATED,
164
0
      "Passing null is deprecated, instead the last opened directory stream should be provided");
165
0
    if (UNEXPECTED(DIRG(default_dir) == NULL)) {
166
0
      zend_type_error("No resource supplied");
167
0
      return NULL;
168
0
    }
169
0
    zend_resource *res = DIRG(default_dir);
170
0
    ZEND_ASSERT(res->type == php_file_le_stream());
171
0
    dir_stream = (php_stream*) res->ptr;
172
0
  }
173
174
140
  if (UNEXPECTED((dir_stream->flags & PHP_STREAM_FLAG_IS_DIR)) == 0) {
175
0
    zend_argument_type_error(1, "must be a valid Directory resource");
176
0
    return NULL;
177
0
  }
178
140
  return dir_stream;
179
140
}
180
181
static php_stream* php_dir_get_directory_stream_from_this(zval *this_z)
182
0
{
183
0
  zval *handle_zv = Z_DIRECTORY_HANDLE_P(this_z);
184
0
  if (UNEXPECTED(Z_TYPE_P(handle_zv) != IS_RESOURCE)) {
185
0
    zend_throw_error(NULL, "Internal directory stream has been altered");
186
0
    return NULL;
187
0
  }
188
0
  zend_resource *res = Z_RES_P(handle_zv);
189
  /* Assume the close() method was called
190
   * (instead of the hacky case where a different resource would have been set via the ArrayObject "hack") */
191
0
  if (UNEXPECTED(res->type != php_file_le_stream())) {
192
    /* TypeError is used for BC, TODO: Use base Error in PHP 9 */
193
0
    zend_type_error("Directory::%s(): cannot use Directory resource after it has been closed", get_active_function_name());
194
0
    return NULL;
195
0
  }
196
0
  php_stream *dir_stream = (php_stream*) res->ptr;
197
0
  if (UNEXPECTED((dir_stream->flags & PHP_STREAM_FLAG_IS_DIR)) == 0) {
198
0
    zend_throw_error(NULL, "Internal directory stream has been altered");
199
0
    return NULL;
200
0
  }
201
0
  return dir_stream;
202
0
}
203
204
/* {{{ Close directory connection identified by the dir_handle */
205
PHP_FUNCTION(closedir)
206
140
{
207
140
  php_stream *dirp = NULL;
208
209
420
  ZEND_PARSE_PARAMETERS_START(0, 1)
210
420
    Z_PARAM_OPTIONAL
211
560
    PHP_Z_PARAM_STREAM_OR_NULL(dirp)
212
140
  ZEND_PARSE_PARAMETERS_END();
213
214
140
  dirp = php_dir_get_directory_stream_from_user_arg(dirp);
215
140
  if (UNEXPECTED(dirp == NULL)) {
216
0
    RETURN_THROWS();
217
0
  }
218
140
  zend_resource *res = dirp->res;
219
140
  zend_list_close(res);
220
221
140
  if (res == DIRG(default_dir)) {
222
140
    php_set_default_dir(NULL);
223
140
  }
224
140
}
225
/* }}} */
226
227
PHP_METHOD(Directory, close)
228
0
{
229
0
  ZEND_PARSE_PARAMETERS_NONE();
230
231
0
  php_stream *dirp = php_dir_get_directory_stream_from_this(ZEND_THIS);
232
0
  if (UNEXPECTED(dirp == NULL)) {
233
0
    RETURN_THROWS();
234
0
  }
235
236
0
  zend_resource *res = dirp->res;
237
0
  zend_list_close(res);
238
239
0
  if (res == DIRG(default_dir)) {
240
0
    php_set_default_dir(NULL);
241
0
  }
242
0
}
243
244
/* {{{ Rewind dir_handle back to the start */
245
PHP_FUNCTION(rewinddir)
246
0
{
247
0
  php_stream *dirp = NULL;
248
249
0
  ZEND_PARSE_PARAMETERS_START(0, 1)
250
0
    Z_PARAM_OPTIONAL
251
0
    PHP_Z_PARAM_STREAM_OR_NULL(dirp)
252
0
  ZEND_PARSE_PARAMETERS_END();
253
254
0
  dirp = php_dir_get_directory_stream_from_user_arg(dirp);
255
0
  if (UNEXPECTED(dirp == NULL)) {
256
0
    RETURN_THROWS();
257
0
  }
258
259
0
  php_stream_rewinddir(dirp);
260
0
}
261
/* }}} */
262
263
PHP_METHOD(Directory, rewind)
264
0
{
265
0
  ZEND_PARSE_PARAMETERS_NONE();
266
267
0
  php_stream *dirp = php_dir_get_directory_stream_from_this(ZEND_THIS);
268
0
  if (UNEXPECTED(dirp == NULL)) {
269
0
    RETURN_THROWS();
270
0
  }
271
272
0
  php_stream_rewinddir(dirp);
273
0
}
274
275
/* {{{ Read directory entry from dir_handle */
276
PHP_FUNCTION(readdir)
277
0
{
278
0
  php_stream *dirp = NULL;
279
280
0
  ZEND_PARSE_PARAMETERS_START(0, 1)
281
0
    Z_PARAM_OPTIONAL
282
0
    PHP_Z_PARAM_STREAM_OR_NULL(dirp)
283
0
  ZEND_PARSE_PARAMETERS_END();
284
285
0
  dirp = php_dir_get_directory_stream_from_user_arg(dirp);
286
0
  if (UNEXPECTED(dirp == NULL)) {
287
0
    RETURN_THROWS();
288
0
  }
289
290
0
  php_stream_dirent entry;
291
0
  if (php_stream_readdir(dirp, &entry)) {
292
0
    RETURN_STRING(entry.d_name);
293
0
  }
294
0
  RETURN_FALSE;
295
0
}
296
/* }}} */
297
298
PHP_METHOD(Directory, read)
299
0
{
300
0
  ZEND_PARSE_PARAMETERS_NONE();
301
302
0
  php_stream *dirp = php_dir_get_directory_stream_from_this(ZEND_THIS);
303
0
  if (UNEXPECTED(dirp == NULL)) {
304
0
    RETURN_THROWS();
305
0
  }
306
307
0
  php_stream_dirent entry;
308
0
  if (php_stream_readdir(dirp, &entry)) {
309
0
    RETURN_STRING(entry.d_name);
310
0
  }
311
0
  RETURN_FALSE;
312
0
}
313
314
#if defined(HAVE_CHROOT) && !defined(ZTS) && defined(ENABLE_CHROOT_FUNC)
315
/* {{{ Change root directory */
316
PHP_FUNCTION(chroot)
317
{
318
  char *str;
319
  int ret;
320
  size_t str_len;
321
322
  ZEND_PARSE_PARAMETERS_START(1, 1)
323
    Z_PARAM_PATH(str, str_len)
324
  ZEND_PARSE_PARAMETERS_END();
325
326
  ret = chroot(str);
327
  if (ret != 0) {
328
    php_error_docref(NULL, E_WARNING, "%s (errno %d)", strerror(errno), errno);
329
    RETURN_FALSE;
330
  }
331
332
  php_clear_stat_cache(1, NULL, 0);
333
334
  ret = chdir("/");
335
336
  if (ret != 0) {
337
    php_error_docref(NULL, E_WARNING, "%s (errno %d)", strerror(errno), errno);
338
    RETURN_FALSE;
339
  }
340
341
  RETURN_TRUE;
342
}
343
/* }}} */
344
#endif
345
346
/* {{{ Change the current directory */
347
PHP_FUNCTION(chdir)
348
0
{
349
0
  char *str;
350
0
  int ret;
351
0
  size_t str_len;
352
353
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
354
0
    Z_PARAM_PATH(str, str_len)
355
0
  ZEND_PARSE_PARAMETERS_END();
356
357
0
  if (php_check_open_basedir(str)) {
358
0
    RETURN_FALSE;
359
0
  }
360
0
  ret = VCWD_CHDIR(str);
361
362
0
  if (ret != 0) {
363
0
    php_error_docref(NULL, E_WARNING, "%s (errno %d)", strerror(errno), errno);
364
0
    RETURN_FALSE;
365
0
  }
366
367
0
  if (BG(CurrentStatFile) && !IS_ABSOLUTE_PATH(ZSTR_VAL(BG(CurrentStatFile)), ZSTR_LEN(BG(CurrentStatFile)))) {
368
0
    zend_string_release(BG(CurrentStatFile));
369
0
    BG(CurrentStatFile) = NULL;
370
0
  }
371
0
  if (BG(CurrentLStatFile) && !IS_ABSOLUTE_PATH(ZSTR_VAL(BG(CurrentLStatFile)), ZSTR_LEN(BG(CurrentLStatFile)))) {
372
0
    zend_string_release(BG(CurrentLStatFile));
373
0
    BG(CurrentLStatFile) = NULL;
374
0
  }
375
376
0
  RETURN_TRUE;
377
0
}
378
/* }}} */
379
380
/* {{{ Gets the current directory */
381
PHP_FUNCTION(getcwd)
382
5
{
383
5
  char path[MAXPATHLEN];
384
5
  char *ret=NULL;
385
386
5
  ZEND_PARSE_PARAMETERS_NONE();
387
388
5
#ifdef HAVE_GETCWD
389
5
  ret = VCWD_GETCWD(path, MAXPATHLEN);
390
#elif defined(HAVE_GETWD)
391
  ret = VCWD_GETWD(path);
392
#endif
393
394
5
  if (ret) {
395
5
    RETURN_STRING(path);
396
5
  } else {
397
0
    RETURN_FALSE;
398
0
  }
399
5
}
400
/* }}} */
401
402
/* {{{ Find pathnames matching a pattern */
403
PHP_FUNCTION(glob)
404
0
{
405
0
  size_t cwd_skip = 0;
406
#ifdef ZTS
407
  char cwd[MAXPATHLEN];
408
  char work_pattern[MAXPATHLEN];
409
  char *result;
410
#endif
411
0
  char *pattern = NULL;
412
0
  size_t pattern_len;
413
0
  zend_long flags = 0;
414
0
  php_glob_t globbuf;
415
0
  size_t n;
416
0
  int ret;
417
0
  bool basedir_limit = 0;
418
0
  zval tmp;
419
420
0
  ZEND_PARSE_PARAMETERS_START(1, 2)
421
0
    Z_PARAM_PATH(pattern, pattern_len)
422
0
    Z_PARAM_OPTIONAL
423
0
    Z_PARAM_LONG(flags)
424
0
  ZEND_PARSE_PARAMETERS_END();
425
426
0
  if (pattern_len >= MAXPATHLEN) {
427
0
    php_error_docref(NULL, E_WARNING, "Pattern exceeds the maximum allowed length of %d characters", MAXPATHLEN);
428
0
    RETURN_FALSE;
429
0
  }
430
431
0
  if ((PHP_GLOB_AVAILABLE_FLAGS & flags) != flags) {
432
0
    php_error_docref(NULL, E_WARNING, "At least one of the passed flags is invalid or not supported on this platform");
433
0
    RETURN_FALSE;
434
0
  }
435
436
#ifdef ZTS
437
  if (!IS_ABSOLUTE_PATH(pattern, pattern_len)) {
438
    result = VCWD_GETCWD(cwd, MAXPATHLEN);
439
    if (!result) {
440
      cwd[0] = '\0';
441
    }
442
#ifdef PHP_WIN32
443
    if (IS_SLASH(*pattern)) {
444
      cwd[2] = '\0';
445
    }
446
#endif
447
    cwd_skip = strlen(cwd)+1;
448
449
    snprintf(work_pattern, MAXPATHLEN, "%s%c%s", cwd, DEFAULT_SLASH, pattern);
450
    pattern = work_pattern;
451
  }
452
#endif
453
454
455
0
  memset(&globbuf, 0, sizeof(globbuf));
456
0
  globbuf.gl_offs = 0;
457
0
  if (0 != (ret = php_glob(pattern, flags & PHP_GLOB_FLAGMASK, NULL, &globbuf))) {
458
0
#ifdef PHP_GLOB_NOMATCH
459
0
    if (PHP_GLOB_NOMATCH == ret) {
460
      /* Some glob implementation simply return no data if no matches
461
         were found, others return the PHP_GLOB_NOMATCH error code.
462
         We don't want to treat PHP_GLOB_NOMATCH as an error condition
463
         so that PHP glob() behaves the same on both types of
464
         implementations and so that 'foreach (glob() as ...'
465
         can be used for simple glob() calls without further error
466
         checking.
467
      */
468
0
      goto no_results;
469
0
    }
470
0
#endif
471
0
    RETURN_FALSE;
472
0
  }
473
474
  /* now catch the FreeBSD style of "no matches" */
475
0
  if (!globbuf.gl_pathc || !globbuf.gl_pathv) {
476
0
#ifdef PHP_GLOB_NOMATCH
477
0
no_results:
478
0
#endif
479
0
    RETURN_EMPTY_ARRAY();
480
0
  }
481
482
0
  array_init(return_value);
483
0
  for (n = 0; n < (size_t)globbuf.gl_pathc; n++) {
484
0
    if (PG(open_basedir) && *PG(open_basedir)) {
485
0
      if (php_check_open_basedir_ex(globbuf.gl_pathv[n], 0)) {
486
0
        basedir_limit = 1;
487
0
        continue;
488
0
      }
489
0
    }
490
    /* we need to do this every time since PHP_GLOB_ONLYDIR does not guarantee that
491
     * all directories will be filtered. GNU libc documentation states the
492
     * following:
493
     * If the information about the type of the file is easily available
494
     * non-directories will be rejected but no extra work will be done to
495
     * determine the information for each file. I.e., the caller must still be
496
     * able to filter directories out.
497
     */
498
0
    if (flags & PHP_GLOB_ONLYDIR) {
499
0
      zend_stat_t s = {0};
500
501
0
      if (0 != VCWD_STAT(globbuf.gl_pathv[n], &s)) {
502
0
        continue;
503
0
      }
504
505
0
      if (S_IFDIR != (s.st_mode & S_IFMT)) {
506
0
        continue;
507
0
      }
508
0
    }
509
0
    ZVAL_STRING(&tmp, globbuf.gl_pathv[n]+cwd_skip);
510
0
    zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &tmp);
511
0
  }
512
513
0
  php_globfree(&globbuf);
514
515
0
  if (basedir_limit && !zend_hash_num_elements(Z_ARRVAL_P(return_value))) {
516
0
    zend_array_destroy(Z_ARR_P(return_value));
517
0
    RETURN_FALSE;
518
0
  }
519
0
}
520
/* }}} */
521
522
/* {{{ List files & directories inside the specified path */
523
PHP_FUNCTION(scandir)
524
0
{
525
0
  char *dirn;
526
0
  size_t dirn_len;
527
0
  zend_long flags = PHP_SCANDIR_SORT_ASCENDING;
528
0
  zend_string **namelist;
529
0
  int n, i;
530
0
  zval *zcontext = NULL;
531
0
  php_stream_context *context = NULL;
532
533
0
  ZEND_PARSE_PARAMETERS_START(1, 3)
534
0
    Z_PARAM_PATH(dirn, dirn_len)
535
0
    Z_PARAM_OPTIONAL
536
0
    Z_PARAM_LONG(flags)
537
0
    Z_PARAM_RESOURCE_OR_NULL(zcontext)
538
0
  ZEND_PARSE_PARAMETERS_END();
539
540
0
  if (dirn_len < 1) {
541
0
    zend_argument_must_not_be_empty_error(1);
542
0
    RETURN_THROWS();
543
0
  }
544
545
0
  if (zcontext) {
546
0
    context = php_stream_context_from_zval(zcontext, 0);
547
0
  }
548
549
0
  if (flags == PHP_SCANDIR_SORT_ASCENDING) {
550
0
    n = php_stream_scandir(dirn, &namelist, context, (void *) php_stream_dirent_alphasort);
551
0
  } else if (flags == PHP_SCANDIR_SORT_NONE) {
552
0
    n = php_stream_scandir(dirn, &namelist, context, NULL);
553
0
  } else {
554
0
    n = php_stream_scandir(dirn, &namelist, context, (void *) php_stream_dirent_alphasortr);
555
0
  }
556
0
  if (n < 0) {
557
0
    php_error_docref(NULL, E_WARNING, "(errno %d): %s", errno, strerror(errno));
558
0
    RETURN_FALSE;
559
0
  }
560
561
0
  array_init(return_value);
562
563
0
  for (i = 0; i < n; i++) {
564
0
    add_next_index_str(return_value, namelist[i]);
565
0
  }
566
567
0
  if (n) {
568
    efree(namelist);
569
0
  }
570
0
}
571
/* }}} */