• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JBZoo / Utils / 13023138991

22 Jun 2024 09:31AM UTC coverage: 92.722% (-0.04%) from 92.766%
13023138991

push

github

web-flow
Fix escaping of backslashes in regex and test cases (#53)

6 of 7 new or added lines in 1 file covered. (85.71%)

2 existing lines in 2 files now uncovered.

1669 of 1800 relevant lines covered (92.72%)

41.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.61
/src/FS.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Utils.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Utils
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Utils;
18

19
/**
20
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
21
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
22
 * @SuppressWarnings(PHPMD.ShortClassName)
23
 */
24
final class FS
25
{
26
    public const TYPE_SOCKET    = 0xC000;
27
    public const TYPE_SYMLINK   = 0xA000;
28
    public const TYPE_REGULAR   = 0x8000;
29
    public const TYPE_BLOCK     = 0x6000;
30
    public const TYPE_DIR       = 0x4000;
31
    public const TYPE_CHARACTER = 0x2000;
32
    public const TYPE_FIFO      = 0x1000;
33

34
    public const PERM_OWNER_READ        = 0x0100;
35
    public const PERM_OWNER_WRITE       = 0x0080;
36
    public const PERM_OWNER_EXEC        = 0x0040;
37
    public const PERM_OWNER_EXEC_STICKY = 0x0800;
38

39
    public const PERM_GROUP_READ        = 0x0020;
40
    public const PERM_GROUP_WRITE       = 0x0010;
41
    public const PERM_GROUP_EXEC        = 0x0008;
42
    public const PERM_GROUP_EXEC_STICKY = 0x0400;
43

44
    public const PERM_ALL_READ        = 0x0004;
45
    public const PERM_ALL_WRITE       = 0x0002;
46
    public const PERM_ALL_EXEC        = 0x0001;
47
    public const PERM_ALL_EXEC_STICKY = 0x0200;
48

49
    /**
50
     * Returns the file permissions as a nice string, like -rw-r--r-- or false if the file is not found.
51
     *
52
     * @param string   $file  The name of the file to get permissions form
53
     * @param null|int $perms numerical value of permissions to display as text
54
     *
55
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
56
     * @SuppressWarnings(PHPMD.NPathComplexity)
57
     */
58
    public static function perms(string $file, ?int $perms = null): string
59
    {
60
        if ($perms === null) {
12✔
61
            if (!\file_exists($file)) {
12✔
62
                return '';
6✔
63
            }
64

65
            /** @noinspection CallableParameterUseCaseInTypeContextInspection */
66
            $perms = \fileperms($file);
6✔
67
        }
68

69
        /** @codeCoverageIgnoreStart */
70
        $info = 'u'; // undefined
6✔
71
        if (($perms & self::TYPE_SOCKET) === self::TYPE_SOCKET) {
6✔
72
            $info = 's';
×
73
        } elseif (($perms & self::TYPE_SYMLINK) === self::TYPE_SYMLINK) {
6✔
74
            $info = 'l';
×
75
        } elseif (($perms & self::TYPE_REGULAR) === self::TYPE_REGULAR) {
6✔
76
            $info = '-';
6✔
77
        } elseif (($perms & self::TYPE_BLOCK) === self::TYPE_BLOCK) {
×
78
            $info = 'b';
×
79
        } elseif (($perms & self::TYPE_DIR) === self::TYPE_DIR) {
×
80
            $info = 'd';
×
81
        } elseif (($perms & self::TYPE_CHARACTER) === self::TYPE_CHARACTER) {
×
82
            $info = 'c';
×
83
        } elseif (($perms & self::TYPE_FIFO) === self::TYPE_FIFO) {
×
84
            $info = 'p';
×
85
        }
86
        // @codeCoverageIgnoreEnd
87

88
        // Owner
89
        $info .= (($perms & self::PERM_OWNER_READ) > 0 ? 'r' : '-');
6✔
90
        $info .= (($perms & self::PERM_OWNER_WRITE) > 0 ? 'w' : '-');
6✔
91

92
        /** @noinspection NestedTernaryOperatorInspection */
93
        $info .= (($perms & self::PERM_OWNER_EXEC) > 0
6✔
94
            ? (($perms & self::PERM_OWNER_EXEC_STICKY) > 0 ? 's' : 'x')
×
95
            : (($perms & self::PERM_OWNER_EXEC_STICKY) > 0 ? 'S' : '-'));
6✔
96

97
        // Group
98
        $info .= (($perms & self::PERM_GROUP_READ) > 0 ? 'r' : '-');
6✔
99
        $info .= (($perms & self::PERM_GROUP_WRITE) > 0 ? 'w' : '-');
6✔
100

101
        /** @noinspection NestedTernaryOperatorInspection */
102
        $info .= (($perms & self::PERM_GROUP_EXEC) > 0
6✔
103
            ? (($perms & self::PERM_GROUP_EXEC_STICKY) > 0 ? 's' : 'x')
×
104
            : (($perms & self::PERM_GROUP_EXEC_STICKY) > 0 ? 'S' : '-'));
6✔
105

106
        // All
107
        $info .= (($perms & self::PERM_ALL_READ) > 0 ? 'r' : '-');
6✔
108
        $info .= (($perms & self::PERM_ALL_WRITE) > 0 ? 'w' : '-');
6✔
109

110
        /** @noinspection NestedTernaryOperatorInspection */
111
        $info .= (($perms & self::PERM_ALL_EXEC) > 0
6✔
112
            ? (($perms & self::PERM_ALL_EXEC_STICKY) > 0 ? 't' : 'x')
×
113
            : (($perms & self::PERM_ALL_EXEC_STICKY) > 0 ? 'T' : '-'));
6✔
114

115
        return $info;
6✔
116
    }
117

118
    /**
119
     * Removes a directory (and its contents) recursively.
120
     * Contributed by Askar (ARACOOL) <https://github.com/ARACOOOL>.
121
     * @param string $dir              The directory to be deleted recursively
122
     * @param bool   $traverseSymlinks Delete contents of symlinks recursively
123
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
124
     * @SuppressWarnings(PHPMD.NPathComplexity)
125
     */
126
    public static function rmDir(string $dir, bool $traverseSymlinks = true): bool
127
    {
128
        if (!\file_exists($dir)) {
18✔
129
            return true;
6✔
130
        }
131

132
        if (!\is_dir($dir)) {
18✔
133
            throw new Exception('Given path is not a directory');
6✔
134
        }
135

136
        if ($traverseSymlinks || !\is_link($dir)) {
18✔
137
            $list = (array)\scandir($dir, \SCANDIR_SORT_NONE);
18✔
138

139
            foreach ($list as $file) {
18✔
140
                if ($file === '.' || $file === '..') {
18✔
141
                    continue;
18✔
142
                }
143

144
                $currentPath = "{$dir}/{$file}";
18✔
145

146
                if (\is_dir($currentPath)) {
18✔
147
                    self::rmDir($currentPath, $traverseSymlinks);
6✔
148
                } elseif (!\unlink($currentPath)) {
18✔
149
                    throw new Exception('Unable to delete ' . $currentPath);
×
150
                }
151
            }
152
        }
153

154
        // Windows treats removing directory symlinks identically to removing directories.
155
        if (!\defined('PHP_WINDOWS_VERSION_MAJOR') && \is_link($dir)) {
18✔
156
            if (!\unlink($dir)) {
6✔
UNCOV
157
                throw new Exception('Unable to delete ' . $dir);
2✔
158
            }
159
        } elseif (!\rmdir($dir)) {
18✔
160
            throw new Exception('Unable to delete ' . $dir);
×
161
        }
162

163
        return true;
18✔
164
    }
165

166
    /**
167
     * Binary safe to open file.
168
     * @deprecated Use \file_get_contents()
169
     */
170
    public static function openFile(string $filepath): ?string
171
    {
172
        $contents = null;
6✔
173

174
        $realPath = \realpath($filepath);
6✔
175
        if ($realPath !== false) {
6✔
176
            $handle   = \fopen($realPath, 'r');
6✔
177
            $fileSize = (int)\filesize($realPath);
6✔
178
            if ($fileSize === 0) {
6✔
NEW
179
                return null;
×
180
            }
181

182
            if ($handle !== false) {
6✔
183
                $contents = (string)\fread($handle, $fileSize);
6✔
184
                \fclose($handle);
6✔
185
            }
186
        }
187

188
        return $contents;
6✔
189
    }
190

191
    /**
192
     * Quickest way for getting first file line.
193
     */
194
    public static function firstLine(string $filepath): ?string
195
    {
196
        if (\file_exists($filepath)) {
6✔
197
            $cacheRes = \fopen($filepath, 'r');
6✔
198
            if ($cacheRes !== false) {
6✔
199
                $firstLine = \fgets($cacheRes);
6✔
200
                \fclose($cacheRes);
6✔
201

202
                return $firstLine === false ? null : $firstLine;
6✔
203
            }
204
        }
205

206
        return null;
6✔
207
    }
208

209
    /**
210
     * Set the writable bit on a file to the minimum value that allows the user running PHP to write to it.
211
     * @param string $filename The filename to set the writable bit on
212
     * @param bool   $writable Whether to make the file writable or not
213
     */
214
    public static function writable(string $filename, bool $writable = true): bool
215
    {
216
        return self::setPerms($filename, $writable, 2);
6✔
217
    }
218

219
    /**
220
     * Set the readable bit on a file to the minimum value that allows the user running PHP to read to it.
221
     * @param string $filename The filename to set the readable bit on
222
     * @param bool   $readable Whether to make the file readable or not
223
     */
224
    public static function readable(string $filename, bool $readable = true): bool
225
    {
226
        return self::setPerms($filename, $readable, 4);
6✔
227
    }
228

229
    /**
230
     * Set the executable bit on a file to the minimum value that allows the user running PHP to read to it.
231
     * @param string $filename   The filename to set the executable bit on
232
     * @param bool   $executable Whether to make the file executable or not
233
     */
234
    public static function executable(string $filename, bool $executable = true): bool
235
    {
236
        return self::setPerms($filename, $executable, 1);
6✔
237
    }
238

239
    /**
240
     * Returns size of a given directory in bytes.
241
     */
242
    public static function dirSize(string $dir): int
243
    {
244
        $size = 0;
6✔
245

246
        $flags = \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS;
6✔
247

248
        $dirIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, $flags));
6✔
249

250
        /** @var \SplFileInfo $splFileInfo */
251
        foreach ($dirIterator as $splFileInfo) {
6✔
252
            if ($splFileInfo->isFile()) {
6✔
253
                $size += (int)$splFileInfo->getSize();
6✔
254
            }
255
        }
256

257
        return $size;
6✔
258
    }
259

260
    /**
261
     * Returns all paths inside a directory.
262
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
263
     * @SuppressWarnings(PHPMD.ShortMethodName)
264
     */
265
    public static function ls(string $dir): array
266
    {
267
        $contents = [];
6✔
268

269
        $flags = \FilesystemIterator::KEY_AS_PATHNAME
6✔
270
            | \FilesystemIterator::CURRENT_AS_FILEINFO
6✔
271
            | \FilesystemIterator::SKIP_DOTS;
6✔
272

273
        $dirIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, $flags));
6✔
274

275
        /** @var \SplFileInfo $splFileInfo */
276
        foreach ($dirIterator as $splFileInfo) {
6✔
277
            $contents[] = $splFileInfo->getPathname();
6✔
278
        }
279

280
        \natsort($contents);
6✔
281

282
        return $contents;
6✔
283
    }
284

285
    /**
286
     * Nice formatting for computer sizes (Bytes).
287
     * @param int $bytes    The number in bytes to format
288
     * @param int $decimals The number of decimal points to include
289
     */
290
    public static function format(int $bytes, int $decimals = 2): string
291
    {
292
        $isNegative = $bytes < 0;
12✔
293

294
        $symbols = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
12✔
295

296
        $bytes = \abs((float)$bytes);
12✔
297
        $exp   = (int)\floor(\log($bytes) / \log(1024));
12✔
298
        $value = ($bytes / (1024 ** \floor($exp)));
12✔
299

300
        if ($symbols[$exp] === 'B') {
12✔
301
            $decimals = 0;
6✔
302
        }
303

304
        return ($isNegative ? '-' : '') . \number_format($value, $decimals, '.', '') . ' ' . $symbols[$exp];
12✔
305
    }
306

307
    /**
308
     * Returns extension of file from FS pathname.
309
     */
310
    public static function ext(?string $path): string
311
    {
312
        if (isStrEmpty($path)) {
12✔
313
            return '';
6✔
314
        }
315

316
        if (\str_contains((string)$path, '?')) {
12✔
317
            $path = (string)\preg_replace('#\?(.*)#', '', (string)$path);
6✔
318
        }
319

320
        $ext = \pathinfo((string)$path, \PATHINFO_EXTENSION);
12✔
321

322
        return \strtolower($ext);
12✔
323
    }
324

325
    /**
326
     * Returns name of file with ext from FS pathname.
327
     */
328
    public static function base(?string $path): string
329
    {
330
        return \pathinfo((string)$path, \PATHINFO_BASENAME);
6✔
331
    }
332

333
    /**
334
     * Returns filename without ext from FS pathname.
335
     */
336
    public static function filename(?string $path): string
337
    {
338
        return \pathinfo((string)$path, \PATHINFO_FILENAME);
6✔
339
    }
340

341
    /**
342
     * Returns name for directory from FS pathname.
343
     */
344
    public static function dirName(?string $path): string
345
    {
346
        return \pathinfo((string)$path, \PATHINFO_DIRNAME);
6✔
347
    }
348

349
    /**
350
     * Returns realpath (smart analog of PHP \realpath()).
351
     */
352
    public static function real(?string $path): ?string
353
    {
354
        if (isStrEmpty($path)) {
24✔
355
            return null;
×
356
        }
357

358
        $result = \realpath((string)$path);
24✔
359

360
        return $result === false ? null : $result;
24✔
361
    }
362

363
    /**
364
     * Function to strip trailing / or \ in a pathname.
365
     * @param null|string $path   the path to clean
366
     * @param string      $dirSep directory separator (optional)
367
     * @SuppressWarnings(PHPMD.Superglobals)
368
     */
369
    public static function clean(?string $path, string $dirSep = \DIRECTORY_SEPARATOR): string
370
    {
371
        if (isStrEmpty($path)) {
42✔
372
            return '';
6✔
373
        }
374

375
        $path = \trim((string)$path);
42✔
376

377
        if (($dirSep === '\\') && ($path[0] === '\\') && ($path[1] === '\\')) {
42✔
378
            $path = '\\' . \preg_replace('#[/\\\]+#', $dirSep, $path);
6✔
379
        } else {
380
            $path = (string)\preg_replace('#[/\\\]+#', $dirSep, $path);
42✔
381
        }
382

383
        return $path;
42✔
384
    }
385

386
    /**
387
     * Strip off the extension if it exists.
388
     */
389
    public static function stripExt(string $path): string
390
    {
391
        $reg = '/\.' . \preg_quote(self::ext($path), '') . '$/';
6✔
392

393
        return (string)\preg_replace($reg, '', $path);
6✔
394
    }
395

396
    /**
397
     * Check is current path directory.
398
     */
399
    public static function isDir(?string $path): bool
400
    {
401
        if (isStrEmpty($path)) {
6✔
402
            return false;
×
403
        }
404

405
        $path = self::clean($path);
6✔
406

407
        return \is_dir($path);
6✔
408
    }
409

410
    /**
411
     * Check is current path regular file.
412
     */
413
    public static function isFile(string $path): bool
414
    {
415
        $path = self::clean($path);
6✔
416

417
        return \file_exists($path) && \is_file($path);
6✔
418
    }
419

420
    /**
421
     * Find relative path of file (remove root part).
422
     */
423
    public static function getRelative(
424
        string $path,
425
        ?string $rootPath = null,
426
        string $forceDS = \DIRECTORY_SEPARATOR,
427
    ): string {
428
        // Cleanup file path
429
        $cleanedPath = self::clean((string)self::real($path), $forceDS);
6✔
430

431
        // Cleanup root path
432
        $rootPath = isStrEmpty($rootPath) ? Sys::getDocRoot() : $rootPath;
6✔
433
        $rootPath = self::clean((string)self::real((string)$rootPath), $forceDS);
6✔
434

435
        // Remove root part
436
        $relPath = (string)\preg_replace('#^' . \preg_quote($rootPath, '\\') . '#', '', $cleanedPath);
6✔
437

438
        return \ltrim($relPath, $forceDS);
6✔
439
    }
440

441
    /**
442
     * Returns clean realpath if file or directory exists.
443
     */
444
    public static function isReal(?string $path): bool
445
    {
446
        if (isStrEmpty($path)) {
6✔
447
            return false;
×
448
        }
449

450
        $expected = self::clean((string)self::real($path));
6✔
451
        $actual   = self::clean($path);
6✔
452

453
        return !isStrEmpty($expected) && $expected === $actual;
6✔
454
    }
455

456
    /**
457
     * Returns true if file is writable.
458
     */
459
    private static function setPerms(string $filename, bool $isFlag, int $perm): bool
460
    {
461
        $stat = @\stat($filename);
18✔
462

463
        if ($stat === false) {
18✔
464
            return false;
18✔
465
        }
466

467
        // We're on Windows
468
        if (Sys::isWin()) {
18✔
469
            return true;
×
470
        }
471

472
        [$myuid, $mygid] = [\posix_geteuid(), \posix_getgid()];
18✔
473

474
        $isMyUid = $stat['uid'] === $myuid;
18✔
475
        $isMyGid = $stat['gid'] === $mygid;
18✔
476

477
        if ($isFlag) {
18✔
478
            // Set only the user writable bit (file is owned by us)
479
            if ($isMyUid) {
18✔
480
                return \chmod($filename, \fileperms($filename) | \intval('0' . $perm . '00', 8));
18✔
481
            }
482

483
            // Set only the group writable bit (file group is the same as us)
484
            if ($isMyGid) {
×
485
                return \chmod($filename, \fileperms($filename) | \intval('0' . $perm . $perm . '0', 8));
×
486
            }
487

488
            // Set the world writable bit (file isn't owned or grouped by us)
489
            return \chmod($filename, \fileperms($filename) | \intval('0' . $perm . $perm . $perm, 8));
×
490
        }
491

492
        // Set only the user writable bit (file is owned by us)
493
        if ($isMyUid) {
18✔
494
            $add = \intval("0{$perm}{$perm}{$perm}", 8);
18✔
495

496
            return self::chmod($filename, $perm, $add);
18✔
497
        }
498

499
        // Set only the group writable bit (file group is the same as us)
500
        if ($isMyGid) {
×
501
            $add = \intval("00{$perm}{$perm}", 8);
×
502

503
            return self::chmod($filename, $perm, $add);
×
504
        }
505

506
        // Set the world writable bit (file isn't owned or grouped by us)
507
        $add = \intval("000{$perm}", 8);
×
508

509
        return self::chmod($filename, $perm, $add);
×
510
    }
511

512
    /**
513
     * Chmod alias.
514
     */
515
    private static function chmod(string $filename, int $perm, int $add): bool
516
    {
517
        return \chmod($filename, (\fileperms($filename) | \intval('0' . $perm . $perm . $perm, 8)) ^ $add);
18✔
518
    }
519
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc