Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Path
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
14
100.00% covered (success)
100.00%
1 / 1
 inside
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 below
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 ensureDirectory
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Util;
6
7use Cosray\Exception\RuntimeException;
8
9class Path
10{
11    public static function inside(string $parent, string $child, bool $checkIsFile = false): string
12    {
13        $parent = realpath($parent);
14
15        if (!$parent) {
16            throw new RuntimeException('Parent directory does not exist.');
17        }
18
19        $prefix = rtrim($parent, '\\/') . DIRECTORY_SEPARATOR;
20        $path = realpath($prefix . ltrim($child, '\\/'));
21
22        if (!$path || !str_starts_with($path, $prefix)) {
23            throw new RuntimeException(
24                'File or directory does not exist or is not in the expected location.',
25            );
26        }
27
28        if ($checkIsFile && !is_file($path)) {
29            throw new RuntimeException('Path is not a file: ' . $path);
30        }
31
32        return $path;
33    }
34
35    /**
36     * Resolves a directory configured below `$parent`. Unlike `inside()`, the
37     * directory may resolve elsewhere through a symlink, such as uploads shared
38     * between deployed releases; only `..` segments in `$child` are refused.
39     */
40    public static function below(string $parent, string $child): string
41    {
42        if (in_array('..', explode('/', str_replace('\\', '/', $child)), true)) {
43            throw new RuntimeException('Path leaves its parent directory: ' . $child);
44        }
45
46        $dir = rtrim($parent, '\\/') . DIRECTORY_SEPARATOR . ltrim($child, '\\/');
47        $path = realpath($dir);
48
49        if ($path === false || !is_dir($path)) {
50            throw new RuntimeException('Directory does not exist: ' . $dir);
51        }
52
53        return $path;
54    }
55
56    /**
57     * Creates the directory and its parents unless it exists. Concurrent
58     * requests may create it at the same time; only a directory that still
59     * does not exist afterwards is an error.
60     */
61    public static function ensureDirectory(string $dir, int $mode = 0o755): void
62    {
63        if (is_dir($dir)) {
64            return;
65        }
66
67        // mkdir() warns when another request created the directory first,
68        // and the request's error handler would turn that into an exception.
69        set_error_handler(static fn(): bool => true);
70
71        try {
72            $created = mkdir($dir, $mode, recursive: true);
73        } finally {
74            restore_error_handler();
75        }
76
77        if (!$created && !is_dir($dir)) {
78            throw new RuntimeException('Could not create directory: ' . $dir);
79        }
80    }
81}