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

JBZoo / Utils / 29807313165

20 Jul 2026 06:55PM UTC coverage: 92.619%. Remained the same
29807313165

push

github

web-flow
Merge pull request #57 from JBZoo/release/8.0

8.0.0 — PHP 8.3+ floor & lock-step major

15 of 16 new or added lines in 7 files covered. (93.75%)

23 existing lines in 3 files now uncovered.

1669 of 1802 relevant lines covered (92.62%)

41.2 hits per line

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

81.99
/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
 * @psalm-suppress UnusedClass
24
 */
25
final class FS
26
{
27
    public const TYPE_SOCKET    = 0xC000;
28
    public const TYPE_SYMLINK   = 0xA000;
29
    public const TYPE_REGULAR   = 0x8000;
30
    public const TYPE_BLOCK     = 0x6000;
31
    public const TYPE_DIR       = 0x4000;
32
    public const TYPE_CHARACTER = 0x2000;
33
    public const TYPE_FIFO      = 0x1000;
34

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

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

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

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

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

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

88
        /** @codeCoverageIgnoreEnd */
89

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

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

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

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

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

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

117
        return $info;
6✔
118
    }
119

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

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

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

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

146
                $currentPath = "{$dir}/{$file}";
18✔
147

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

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

165
        return true;
18✔
166
    }
167

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

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

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

190
        return $contents;
6✔
191
    }
192

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

204
                return $firstLine === false ? null : $firstLine;
6✔
205
            }
206
        }
207

208
        return null;
6✔
209
    }
210

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

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

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

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

248
        $flags = \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS;
6✔
249

250
        $dirIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, $flags));
6✔
251

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

259
        return $size;
6✔
260
    }
261

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

271
        $flags = \FilesystemIterator::KEY_AS_PATHNAME
6✔
272
            | \FilesystemIterator::CURRENT_AS_FILEINFO
6✔
273
            | \FilesystemIterator::SKIP_DOTS;
6✔
274

275
        $dirIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, $flags));
6✔
276

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

282
        \natsort($contents);
6✔
283

284
        return $contents;
6✔
285
    }
286

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

296
        $symbols = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
6✔
297

298
        $bytes = \abs((float)$bytes);
6✔
299
        // log(0) is -INF; casting -INF to int emits a warning on PHP 8.5. Zero bytes => "0 B".
300
        $exp   = $bytes > 0.0 ? (int)\floor(\log($bytes) / \log(1024)) : 0;
6✔
301
        $value = ($bytes / (1024 ** \floor($exp)));
6✔
302

303
        if ($symbols[$exp] === 'B') {
6✔
304
            $decimals = 0;
6✔
305
        }
306

307
        return ($isNegative ? '-' : '') . \number_format($value, $decimals, '.', '') . ' ' . $symbols[$exp];
6✔
308
    }
309

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

319
        if (\str_contains((string)$path, '?')) {
12✔
320
            $path = (string)\preg_replace('#\?(.*)#', '', (string)$path);
6✔
321
        }
322

323
        $ext = \pathinfo((string)$path, \PATHINFO_EXTENSION);
12✔
324

325
        return \strtolower($ext);
12✔
326
    }
327

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

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

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

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

361
        $result = \realpath((string)$path);
18✔
362

363
        return $result === false ? null : $result;
18✔
364
    }
365

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

378
        $path = \trim((string)$path);
36✔
379

380
        if (($dirSep === '\\') && ($path[0] === '\\') && ($path[1] === '\\')) {
36✔
381
            $path = '\\' . \preg_replace('#[/\\\]+#', $dirSep, $path);
6✔
382
        } else {
383
            $path = (string)\preg_replace('#[/\\\]+#', $dirSep, $path);
36✔
384
        }
385

386
        return $path;
36✔
387
    }
388

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

396
        return (string)\preg_replace($reg, '', $path);
6✔
397
    }
398

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

408
        $path = self::clean($path);
6✔
409

410
        return \is_dir($path);
6✔
411
    }
412

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

420
        return \file_exists($path) && \is_file($path);
6✔
421
    }
422

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

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

438
        // Remove root part
439
        $relPath = (string)\preg_replace('#^' . \preg_quote($rootPath, '\\') . '#', '', $cleanedPath);
6✔
440

441
        return \ltrim($relPath, $forceDS);
6✔
442
    }
443

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

453
        $expected = self::clean((string)self::real($path));
6✔
454
        $actual   = self::clean($path);
6✔
455

456
        return !isStrEmpty($expected) && $expected === $actual;
6✔
457
    }
458

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

466
        if ($stat === false) {
18✔
467
            return false;
18✔
468
        }
469

470
        // We're on Windows
471
        if (Sys::isWin()) {
18✔
UNCOV
472
            return true;
×
473
        }
474

475
        [$myuid, $mygid] = [\posix_geteuid(), \posix_getgid()];
18✔
476

477
        $isMyUid = $stat['uid'] === $myuid;
18✔
478
        $isMyGid = $stat['gid'] === $mygid;
18✔
479

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

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

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

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

499
            return self::chmod($filename, $perm, $add);
18✔
500
        }
501

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

UNCOV
506
            return self::chmod($filename, $perm, $add);
×
507
        }
508

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

UNCOV
512
        return self::chmod($filename, $perm, $add);
×
513
    }
514

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

© 2026 Coveralls, Inc