22declare(strict_types=1);
27use Symfony\Component\Filesystem\Path;
32use
function file_exists;
33use
function file_get_contents;
34use
function file_put_contents;
37use
function ftruncate;
44use
function preg_match;
50use
function str_replace;
51use
function str_starts_with;
52use
function stream_get_contents;
56use
const DIRECTORY_SEPARATOR;
61use
const SCANDIR_SORT_NONE;
65 private static array $lockFileHandles = [];
70 private static array $cleanedPaths = [
71 \pocketmine\PATH => self::CLEAN_PATH_SRC_PREFIX
74 public const CLEAN_PATH_SRC_PREFIX =
"pmsrc";
75 public const CLEAN_PATH_PLUGINS_PREFIX =
"plugins";
77 private function __construct(){
81 public static function recursiveUnlink(
string $dir) :
void{
83 $objects =
Utils::assumeNotFalse(scandir($dir, SCANDIR_SORT_NONE),
"scandir() shouldn't return false when is_dir() returns true");
84 foreach($objects as $object){
85 if($object !==
"." && $object !==
".."){
86 $fullObject = Path::join($dir, $object);
87 if(is_dir($fullObject)){
88 self::recursiveUnlink($fullObject);
95 }elseif(is_file($dir)){
103 public static function recursiveCopy(
string $origin,
string $destination) : void{
104 if(!is_dir($origin)){
105 throw new \RuntimeException(
"$origin does not exist, or is not a directory");
107 if(!is_dir($destination)){
108 if(file_exists($destination)){
109 throw new \RuntimeException(
"$destination already exists, and is not a directory");
111 if(!is_dir(dirname($destination))){
113 throw new \RuntimeException(
"The parent directory of $destination does not exist, or is not a directory");
117 }
catch(\ErrorException $e){
118 if(!is_dir($destination)){
119 throw new \RuntimeException(
"Failed to create output directory $destination: " . $e->getMessage());
123 self::recursiveCopyInternal($origin, $destination);
126 private static function recursiveCopyInternal(
string $origin,
string $destination) : void{
128 if(!is_dir($destination)){
129 if(file_exists($destination)){
130 throw new \RuntimeException(
"Path $destination does not exist, or is not a directory");
135 foreach($objects as $object){
136 if($object ===
"." || $object ===
".."){
139 self::recursiveCopyInternal(Path::join($origin, $object), Path::join($destination, $object));
142 $dirName = dirname($destination);
143 if(!is_dir($dirName)){
144 throw new AssumptionFailedError(
"The destination folder should have been created in the parent call");
146 copy($origin, $destination);
150 public static function addCleanedPath(
string $path,
string $replacement) : void{
151 self::$cleanedPaths[$path] = $replacement;
152 uksort(self::$cleanedPaths,
function(
string $str1,
string $str2) :
int{
153 return strlen($str2) <=> strlen($str1);
163 public static function cleanPath(
string $path) : string{
164 $result = str_replace([DIRECTORY_SEPARATOR,
".php",
"phar://"], [
"/",
"",
""], $path);
168 foreach(Utils::stringifyKeys(self::$cleanedPaths) as $cleanPath => $replacement){
169 $cleanPath = rtrim(str_replace([DIRECTORY_SEPARATOR,
"phar://"], [
"/",
""], $cleanPath),
"/");
170 if(str_starts_with($result, $cleanPath)){
171 $result = ltrim(str_replace($cleanPath, $replacement, $result),
"/");
188 }
catch(\ErrorException $e){
189 throw new \InvalidArgumentException(
"Failed to open lock file: " . $e->getMessage(), 0, $e);
191 if(!flock($resource, LOCK_EX | LOCK_NB)){
194 flock($resource, LOCK_SH);
195 $pid = Utils::assumeNotFalse(stream_get_contents($resource),
"This is a known valid file resource, at worst we should receive an empty string");
196 if(preg_match(
'/^\d+$/', $pid) === 1){
201 ftruncate($resource, 0);
202 fwrite($resource, (
string) getmypid());
204 flock($resource, LOCK_SH);
205 self::$lockFileHandles[realpath($lockFilePath)] = $resource;
215 $lockFilePath = realpath($lockFilePath);
216 if($lockFilePath ===
false){
217 throw new \InvalidArgumentException(
"Invalid lock file path");
219 if(isset(self::$lockFileHandles[$lockFilePath])){
220 flock(self::$lockFileHandles[$lockFilePath], LOCK_UN);
221 fclose(self::$lockFileHandles[$lockFilePath]);
222 unset(self::$lockFileHandles[$lockFilePath]);
223 @unlink($lockFilePath);
238 public static function safeFilePutContents(
string $fileName,
string $contents,
int $flags = 0, $context =
null) : void{
239 $directory = dirname($fileName);
240 if(!is_dir($directory)){
241 throw new \RuntimeException(
"Target directory path does not exist or is not a directory");
243 if(is_dir($fileName)){
244 throw new \RuntimeException(
"Target file path already exists and is not a file");
250 $temporaryFileName = $fileName .
".$counter.tmp";
252 }
while(is_dir($temporaryFileName));
255 ErrorToExceptionHandler::trap(fn() => $context !==
null ?
256 file_put_contents($temporaryFileName, $contents, $flags, $context) :
257 file_put_contents($temporaryFileName, $contents, $flags)
259 }
catch(\ErrorException $filePutContentsException){
261 @unlink($temporaryFileName, $context) :
262 @unlink($temporaryFileName);
263 throw new \RuntimeException(
"Failed to write to temporary file $temporaryFileName: " . $filePutContentsException->getMessage(), 0, $filePutContentsException);
266 $renameTemporaryFileResult = $context !==
null ?
267 @rename($temporaryFileName, $fileName, $context) :
268 @rename($temporaryFileName, $fileName);
269 if(!$renameTemporaryFileResult){
285 ErrorToExceptionHandler::trap(fn() => $context !==
null ?
286 copy($temporaryFileName, $fileName, $context) :
287 copy($temporaryFileName, $fileName)
289 }
catch(\ErrorException $copyException){
290 throw new \RuntimeException(
"Failed to move temporary file contents into target file: " . $copyException->getMessage(), 0, $copyException);
292 @unlink($temporaryFileName);
305 public static function fileGetContents(
string $fileName,
bool $useIncludePath =
false, $context =
null,
int $offset = 0, ?
int $length =
null) : string{
307 return
ErrorToExceptionHandler::trapAndRemoveFalse(fn() => file_get_contents($fileName, $useIncludePath, $context, $offset, $length));
308 }
catch(\ErrorException $e){
309 throw new \RuntimeException(
"Failed to read file $fileName: " . $e->getMessage(), 0, $e);
static trap(\Closure $closure, int $levels=E_WARNING|E_NOTICE)
static recursiveCopy(string $origin, string $destination)
static fileGetContents(string $fileName, bool $useIncludePath=false, $context=null, int $offset=0, ?int $length=null)
static createLockFile(string $lockFilePath)
static releaseLockFile(string $lockFilePath)
static safeFilePutContents(string $fileName, string $contents, int $flags=0, $context=null)
static assumeNotFalse(mixed $value, \Closure|string $context="This should never be false")