Page Menu
Home
WMGMC Issues
搜索
Configure Global Search
登录
Files
F15866
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
订阅
标记用于日后
授予令牌
Size
45 KB
Referenced Files
None
订阅者
None
View Options
diff --git a/.arclint b/.arclint
index bcfb4034d3..2cbad29f39 100644
--- a/.arclint
+++ b/.arclint
@@ -1,69 +1,69 @@
{
"exclude": [
"(^externals/)"
],
"linters": {
"chmod": {
"type": "chmod"
},
"filename": {
"type": "filename"
},
"generated": {
"type": "generated"
},
"json": {
"type": "json",
"include": [
"(^resources/arclint/.*\\.arclint\\.example$)",
"(^\\.arcconfig$)",
"(^\\.arclint$)",
"(\\.json$)"
]
},
"merge-conflict": {
"type": "merge-conflict"
},
"nolint": {
"type": "nolint"
},
"phutil-library": {
"type": "phutil-library",
"include": "(\\.php$)"
},
"spelling": {
"type": "spelling",
"exclude": "(^resources/spelling/.*\\.json$)"
},
"text": {
"type": "text",
"exclude": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect)|/Makefile\\z)"
]
},
"text-without-length": {
"type": "text",
"include": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))"
],
"severity": {
"3": "disabled"
}
},
"text-without-tabs": {
"type": "text",
"include": [
"(/Makefile\\z)"
],
"severity": {
"2": "disabled"
}
},
"xhpast": {
"type": "xhpast",
"include": "(\\.php$)",
"standard": "phutil.xhpast",
- "xhpast.php-version": "5.5.0"
+ "xhpast.php-version": "7.2.25"
}
}
}
diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php
index 74e42d6c47..cf53fdcd1e 100644
--- a/src/filesystem/Filesystem.php
+++ b/src/filesystem/Filesystem.php
@@ -1,1311 +1,1235 @@
<?php
/**
* Simple wrapper class for common filesystem tasks like reading and writing
* files. When things go wrong, this class throws detailed exceptions with
* good information about what didn't work.
*
* Filesystem will resolve relative paths against PWD from the environment.
* When Filesystem is unable to complete an operation, it throws a
* FilesystemException.
*
* @task directory Directories
* @task file Files
* @task path Paths
* @task exec Executables
* @task assert Assertions
*/
final class Filesystem extends Phobject {
/* -( Files )-------------------------------------------------------------- */
/**
* Read a file in a manner similar to file_get_contents(), but throw detailed
* exceptions on failure.
*
* @param string $path File path to read. This file must exist and be
* readable, or an exception will be thrown.
* @return string Contents of the specified file.
*
* @task file
*/
public static function readFile($path) {
$path = self::resolvePath($path);
self::assertExists($path);
self::assertIsFile($path);
self::assertReadable($path);
$data = @file_get_contents($path);
if ($data === false) {
throw new FilesystemException(
$path,
pht("Failed to read file '%s'.", $path));
}
return $data;
}
/**
* Make assertions about the state of path in preparation for
* writeFile() and writeFileIfChanged().
*/
public static function assertWritableFile($path) {
$path = self::resolvePath($path);
$dir = dirname($path);
self::assertExists($dir);
self::assertIsDirectory($dir);
// File either needs to not exist and have a writable parent, or be
// writable itself.
$exists = true;
try {
self::assertNotExists($path);
$exists = false;
} catch (Exception $ex) {
self::assertWritable($path);
}
if (!$exists) {
self::assertWritable($dir);
}
}
/**
* Write a file in a manner similar to file_put_contents(), but throw
* detailed exceptions on failure. If the file already exists, it will be
* overwritten.
*
* @param string $path File path to write. This file must be writable and
* its parent directory must exist.
* @param string $data Data to write.
*
* @task file
*/
public static function writeFile($path, $data) {
self::assertWritableFile($path);
if (@file_put_contents($path, $data) === false) {
throw new FilesystemException(
$path,
pht("Failed to write file '%s'.", $path));
}
}
/**
* Write a file in a manner similar to `file_put_contents()`, but only touch
* the file if the contents are different, and throw detailed exceptions on
* failure.
*
* As this function is used in build steps to update code, if we write a new
* file, we do so by writing to a temporary file and moving it into place.
* This allows a concurrently reading process to see a consistent view of the
* file without needing locking; any given read of the file is guaranteed to
* be self-consistent and not see partial file contents.
*
* @param string $path file path to write
* @param string $data data to write
*
* @return boolean indicating whether the file was changed by this function.
*/
public static function writeFileIfChanged($path, $data) {
if (file_exists($path)) {
$current = self::readFile($path);
if ($current === $data) {
return false;
}
}
self::assertWritableFile($path);
// Create the temporary file alongside the intended destination,
// as this ensures that the rename() will be atomic (on the same fs)
$dir = dirname($path);
$temp = tempnam($dir, 'GEN');
if (!$temp) {
throw new FilesystemException(
$dir,
pht('Unable to create temporary file in %s.', $dir));
}
try {
self::writeFile($temp, $data);
// tempnam will always restrict ownership to us, broaden
// it so that these files respect the actual umask
self::changePermissions($temp, 0666 & ~umask());
// This will appear atomic to concurrent readers
$ok = rename($temp, $path);
if (!$ok) {
throw new FilesystemException(
$path,
pht('Unable to move %s to %s.', $temp, $path));
}
} catch (Exception $e) {
// Make best effort to remove temp file
unlink($temp);
throw $e;
}
return true;
}
/**
* Write data to unique file, without overwriting existing files. This is
* useful if you want to write a ".bak" file or something similar, but want
* to make sure you don't overwrite something already on disk.
*
* This function will add a number to the filename if the base name already
* exists, e.g. "example.bak", "example.bak.1", "example.bak.2", etc. (Don't
* rely on this exact behavior, of course.)
*
* @param string $base Suggested filename, like "example.bak". This name
* will be used if it does not exist, or some similar name
* will be chosen if it does.
* @param string $data Data to write to the file.
* @return string Path to a newly created and written file which did not
* previously exist, like "example.bak.3".
* @task file
*/
public static function writeUniqueFile($base, $data) {
$full_path = self::resolvePath($base);
$sequence = 0;
assert_stringlike($data);
// Try 'file', 'file.1', 'file.2', etc., until something doesn't exist.
while (true) {
$try_path = $full_path;
if ($sequence) {
$try_path .= '.'.$sequence;
}
$handle = @fopen($try_path, 'x');
if ($handle) {
$ok = fwrite($handle, $data);
if ($ok === false) {
throw new FilesystemException(
$try_path,
pht('Failed to write file data.'));
}
$ok = fclose($handle);
if (!$ok) {
throw new FilesystemException(
$try_path,
pht('Failed to close file handle.'));
}
return $try_path;
}
$sequence++;
}
}
/**
* Append to a file without having to deal with file handles, with
* detailed exceptions on failure.
*
* @param string $path File path to write. This file must be writable or
* its parent directory must exist and be writable.
* @param string $data Data to write.
*
* @task file
*/
public static function appendFile($path, $data) {
$path = self::resolvePath($path);
// Use self::writeFile() if the file doesn't already exist
try {
self::assertExists($path);
} catch (FilesystemException $ex) {
self::writeFile($path, $data);
return;
}
// File needs to exist or the directory needs to be writable
$dir = dirname($path);
self::assertExists($dir);
self::assertIsDirectory($dir);
self::assertWritable($dir);
assert_stringlike($data);
if (($fh = fopen($path, 'a')) === false) {
throw new FilesystemException(
$path,
pht("Failed to open file '%s'.", $path));
}
$dlen = strlen($data);
if (fwrite($fh, $data) !== $dlen) {
throw new FilesystemException(
$path,
pht("Failed to write %d bytes to '%s'.", $dlen, $path));
}
if (!fflush($fh) || !fclose($fh)) {
throw new FilesystemException(
$path,
pht("Failed closing file '%s' after write.", $path));
}
}
/**
* Copy a file, preserving file attributes (if relevant for the OS).
*
* @param string $from File path to copy from. This file must exist and be
* readable, or an exception will be thrown.
* @param string $to File path to copy to. If a file exists at this path
* already, it wll be overwritten.
*
* @task file
*/
public static function copyFile($from, $to) {
$from = self::resolvePath($from);
$to = self::resolvePath($to);
self::assertExists($from);
self::assertIsFile($from);
self::assertReadable($from);
if (phutil_is_windows()) {
$trap = new PhutilErrorTrap();
$ok = @copy($from, $to);
$err = $trap->getErrorsAsString();
$trap->destroy();
if (!$ok) {
if ($err !== null && strlen($err)) {
throw new FilesystemException(
$to,
pht(
'Failed to copy file from "%s" to "%s": %s',
$from,
$to,
$err));
} else {
throw new FilesystemException(
$to,
pht(
'Failed to copy file from "%s" to "%s".',
$from,
$to));
}
}
} else {
execx('cp -p %s %s', $from, $to);
}
}
/**
* Remove a file or directory.
*
* @param string $path File to a path or directory to remove.
* @return void
*
* @task file
*/
public static function remove($path) {
if ($path == null || !strlen($path)) {
// Avoid removing PWD.
throw new Exception(
pht(
'No path provided to %s.',
__FUNCTION__.'()'));
}
$path = self::resolvePath($path);
if (!file_exists($path)) {
return;
}
self::executeRemovePath($path);
}
/**
* Rename a file or directory.
*
* @param string $old Old path.
* @param string $new New path.
*
* @task file
*/
public static function rename($old, $new) {
$old = self::resolvePath($old);
$new = self::resolvePath($new);
self::assertExists($old);
$ok = rename($old, $new);
if (!$ok) {
throw new FilesystemException(
$new,
pht("Failed to rename '%s' to '%s'!", $old, $new));
}
}
/**
* Internal. Recursively remove a file or an entire directory. Implements
* the core function of @{method:remove} in a way that works on Windows.
*
* @param string $path File to a path or directory to remove.
* @return void
*
* @task file
*/
private static function executeRemovePath($path) {
if (is_dir($path) && !is_link($path)) {
foreach (self::listDirectory($path, true) as $child) {
self::executeRemovePath($path.DIRECTORY_SEPARATOR.$child);
}
$ok = rmdir($path);
if (!$ok) {
throw new FilesystemException(
$path,
pht("Failed to remove directory '%s'!", $path));
}
} else {
$ok = unlink($path);
if (!$ok) {
throw new FilesystemException(
$path,
pht("Failed to remove file '%s'!", $path));
}
}
}
/**
* Change the permissions of a file or directory.
*
* @param string $path Path to the file or directory.
* @param int $umask Permission umask. Note that umask is in octal, so
* you should specify it as, e.g., `0777', not `777'.
* @return void
*
* @task file
*/
public static function changePermissions($path, $umask) {
$path = self::resolvePath($path);
self::assertExists($path);
if (!@chmod($path, $umask)) {
$readable_umask = sprintf('%04o', $umask);
throw new FilesystemException(
$path,
pht("Failed to chmod '%s' to '%s'.", $path, $readable_umask));
}
}
/**
* Get the last modified time of a file
*
* @param string $path Path to file
* @return int Time last modified
*
* @task file
*/
public static function getModifiedTime($path) {
$path = self::resolvePath($path);
self::assertExists($path);
self::assertIsFile($path);
self::assertReadable($path);
$modified_time = @filemtime($path);
if ($modified_time === false) {
throw new FilesystemException(
$path,
pht('Failed to read modified time for %s.', $path));
}
return $modified_time;
}
/**
* Read random bytes from /dev/urandom or equivalent. See also
* @{method:readRandomCharacters}.
*
* @param int $number_of_bytes Number of bytes to read.
* @return string Random bytestring of the provided length.
*
* @task file
*/
public static function readRandomBytes($number_of_bytes) {
$number_of_bytes = (int)$number_of_bytes;
if ($number_of_bytes < 1) {
throw new Exception(pht('You must generate at least 1 byte of entropy.'));
}
- // Under PHP 7.2.0 and newer, we have a reasonable builtin. For older
- // versions, we fall back to various sources which have a roughly similar
- // effect.
- if (function_exists('random_bytes')) {
- return random_bytes($number_of_bytes);
- }
-
- // Try to use `openssl_random_pseudo_bytes()` if it's available. This source
- // is the most widely available source, and works on Windows/Linux/OSX/etc.
-
- if (function_exists('openssl_random_pseudo_bytes')) {
- $strong = true;
- $data = openssl_random_pseudo_bytes($number_of_bytes, $strong);
-
- if (!$strong) {
- // NOTE: This indicates we're using a weak random source. This is
- // probably OK, but maybe we should be more strict here.
- }
-
- if ($data === false) {
- throw new Exception(
- pht(
- '%s failed to generate entropy!',
- 'openssl_random_pseudo_bytes()'));
- }
-
- if (strlen($data) != $number_of_bytes) {
- throw new Exception(
- pht(
- '%s returned an unexpected number of bytes (got %s, expected %s)!',
- 'openssl_random_pseudo_bytes()',
- new PhutilNumber(strlen($data)),
- new PhutilNumber($number_of_bytes)));
- }
-
- return $data;
- }
-
-
- // Try to use `/dev/urandom` if it's available. This is usually available
- // on non-Windows systems, but some PHP config (open_basedir) and chrooting
- // may limit our access to it.
-
- $urandom = @fopen('/dev/urandom', 'rb');
- if ($urandom) {
- $data = @fread($urandom, $number_of_bytes);
- @fclose($urandom);
- if (strlen($data) != $number_of_bytes) {
- throw new FilesystemException(
- '/dev/urandom',
- pht('Failed to read random bytes!'));
- }
- return $data;
- }
-
- // (We might be able to try to generate entropy here from a weaker source
- // if neither of the above sources panned out, see some discussion in
- // T4153.)
-
- // We've failed to find any valid entropy source. Try to fail in the most
- // useful way we can, based on the platform.
-
- if (phutil_is_windows()) {
- throw new Exception(
- pht(
- '%s requires the PHP OpenSSL extension to be installed and enabled '.
- 'to access an entropy source. On Windows, this extension is usually '.
- 'installed but not enabled by default. Enable it in your "php.ini".',
- __METHOD__.'()'));
- }
-
- throw new Exception(
- pht(
- '%s requires the PHP OpenSSL extension or access to "%s". Install or '.
- 'enable the OpenSSL extension, or make sure "%s" is accessible.',
- __METHOD__.'()',
- '/dev/urandom',
- '/dev/urandom'));
+ // Since PHP 7.2.0, we have a reasonable builtin:
+ return random_bytes($number_of_bytes);
}
/**
* Read random alphanumeric characters from /dev/urandom or equivalent. This
* method operates like @{method:readRandomBytes} but produces alphanumeric
* output (a-z, 0-9) so it's appropriate for use in URIs and other contexts
* where it needs to be human readable.
*
* @param int $number_of_characters Number of characters to read.
* @return string Random character string of the provided length.
*
* @task file
*/
public static function readRandomCharacters($number_of_characters) {
// NOTE: To produce the character string, we generate a random byte string
// of the same length, select the high 5 bits from each byte, and
// map that to 32 alphanumeric characters. This could be improved (we
// could improve entropy per character with base-62, and some entropy
// sources might be less entropic if we discard the low bits) but for
// reasonable cases where we have a good entropy source and are just
// generating some kind of human-readable secret this should be more than
// sufficient and is vastly simpler than trying to do bit fiddling.
$map = array_merge(range('a', 'z'), range('2', '7'));
$result = '';
$bytes = self::readRandomBytes($number_of_characters);
for ($ii = 0; $ii < $number_of_characters; $ii++) {
$result .= $map[ord($bytes[$ii]) >> 3];
}
return $result;
}
/**
* Generate a random integer value in a given range.
*
* This method uses less-entropic random sources under older versions of PHP.
*
* @param int $min Minimum value, inclusive.
* @param int $max Maximum value, inclusive.
*/
public static function readRandomInteger($min, $max) {
if (!is_int($min)) {
throw new Exception(pht('Minimum value must be an integer.'));
}
if (!is_int($max)) {
throw new Exception(pht('Maximum value must be an integer.'));
}
if ($min > $max) {
throw new Exception(
pht(
'Minimum ("%d") must not be greater than maximum ("%d").',
$min,
$max));
}
// Under PHP 7.2.0 and newer, we can just use "random_int()". This function
// is intended to generate cryptographically usable entropy.
if (function_exists('random_int')) {
return random_int($min, $max);
}
// We could find a stronger source for this, but correctly converting raw
// bytes to an integer range without biases is fairly hard and it seems
// like we're more likely to get that wrong than suffer a PRNG prediction
// issue by falling back to "mt_rand()".
if (($max - $min) > mt_getrandmax()) {
throw new Exception(
pht('mt_rand() range is smaller than the requested range.'));
}
$result = mt_rand($min, $max);
if (!is_int($result)) {
throw new Exception(pht('Bad return value from mt_rand().'));
}
return $result;
}
/**
* Identify the MIME type of a file. This returns only the MIME type (like
* text/plain), not the encoding (like charset=utf-8).
*
* @param string $path Path to the file to examine.
* @param string $default (optional) default mime type to return if the
* file's mime type can not be identified.
* @return string File mime type.
*
* @task file
*
* @phutil-external-symbol function mime_content_type
* @phutil-external-symbol function finfo_open
* @phutil-external-symbol function finfo_file
*/
public static function getMimeType(
$path,
$default = 'application/octet-stream') {
$path = self::resolvePath($path);
self::assertExists($path);
self::assertIsFile($path);
self::assertReadable($path);
$mime_type = null;
// Fileinfo is the best approach since it doesn't rely on `file`, but
// it isn't builtin for older versions of PHP.
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME);
if ($finfo) {
$result = finfo_file($finfo, $path);
if ($result !== false) {
$mime_type = $result;
}
}
}
// If we failed Fileinfo, try `file`. This works well but not all systems
// have the binary.
if ($mime_type === null) {
list($err, $stdout) = exec_manual(
'file --brief --mime %s',
$path);
if (!$err) {
$mime_type = trim($stdout);
}
}
// If we didn't get anywhere, try the deprecated mime_content_type()
// function.
if ($mime_type === null) {
if (function_exists('mime_content_type')) {
$result = mime_content_type($path);
if ($result !== false) {
$mime_type = $result;
}
}
}
// If we come back with an encoding, strip it off.
if ($mime_type !== null && strpos($mime_type, ';') !== false) {
list($type, $encoding) = explode(';', $mime_type, 2);
$mime_type = $type;
}
if ($mime_type === null) {
$mime_type = $default;
}
return $mime_type;
}
/* -( Directories )-------------------------------------------------------- */
/**
* Create a directory in a manner similar to mkdir(), but throw detailed
* exceptions on failure.
*
* @param string $path Path to directory. The parent directory must exist
* and be writable.
* @param int $umask Permission umask. Note that umask is in octal, so
* you should specify it as, e.g., `0777', not `777'.
* @param boolean $recursive (optional) Recursively create directories.
* Defaults to false.
* @return string Path to the created directory.
*
* @task directory
*/
public static function createDirectory(
$path,
$umask = 0755,
$recursive = false) {
$path = self::resolvePath($path);
if (is_dir($path)) {
if ($umask) {
self::changePermissions($path, $umask);
}
return $path;
}
$dir = dirname($path);
if ($recursive && !file_exists($dir)) {
// Note: We could do this with the recursive third parameter of mkdir(),
// but then we loose the helpful FilesystemExceptions we normally get.
self::createDirectory($dir, $umask, true);
}
self::assertIsDirectory($dir);
self::assertExists($dir);
self::assertWritable($dir);
self::assertNotExists($path);
if (!mkdir($path, $umask)) {
throw new FilesystemException(
$path,
pht("Failed to create directory '%s'.", $path));
}
// Need to change permissions explicitly because mkdir does something
// slightly different. mkdir(2) man page:
// 'The parameter mode specifies the permissions to use. It is modified by
// the process's umask in the usual way: the permissions of the created
// directory are (mode & ~umask & 0777)."'
if ($umask) {
self::changePermissions($path, $umask);
}
return $path;
}
/**
* Create a temporary directory and return the path to it. You are
* responsible for removing it (e.g., with Filesystem::remove())
* when you are done with it.
*
* @param string $prefix (optional) directory prefix.
* @param int $umask (optional) Permissions to create the directory
* with. By default, these permissions are very restrictive
* (0700).
* @param string $root_directory (optional) Root directory. If not
* provided, the system temporary directory (often "/tmp")
* will be used.
* @return string Path to newly created temporary directory.
*
* @task directory
*/
public static function createTemporaryDirectory(
$prefix = '',
$umask = 0700,
$root_directory = null) {
$prefix = preg_replace('/[^A-Z0-9._-]+/i', '', $prefix);
if ($root_directory !== null) {
$tmp = $root_directory;
self::assertExists($tmp);
self::assertIsDirectory($tmp);
self::assertWritable($tmp);
} else {
$tmp = sys_get_temp_dir();
if (!$tmp) {
throw new FilesystemException(
$tmp,
pht('Unable to determine system temporary directory.'));
}
}
$base = $tmp.DIRECTORY_SEPARATOR.$prefix;
$tries = 3;
do {
$dir = $base.substr(base_convert(md5((string)mt_rand()), 16, 36), 0, 16);
try {
self::createDirectory($dir, $umask);
break;
} catch (FilesystemException $ex) {
// Ignore.
}
} while (--$tries);
if (!$tries) {
$df = disk_free_space($tmp);
if ($df !== false && $df < 1024 * 1024) {
throw new FilesystemException(
$dir,
pht('Failed to create a temporary directory: the disk is full.'));
}
throw new FilesystemException(
$dir,
pht("Failed to create a temporary directory in '%s'.", $tmp));
}
return $dir;
}
/**
* List files in a directory.
*
* @param string $path Path, absolute or relative to PWD.
* @param bool $include_hidden If false, exclude files beginning with
* a ".".
*
* @return array List of files and directories in the specified
* directory, excluding `.' and `..'.
*
* @task directory
*/
public static function listDirectory($path, $include_hidden = true) {
$path = self::resolvePath($path);
self::assertExists($path);
self::assertIsDirectory($path);
self::assertReadable($path);
$list = @scandir($path);
if ($list === false) {
throw new FilesystemException(
$path,
pht("Unable to list contents of directory '%s'.", $path));
}
foreach ($list as $k => $v) {
if ($v == '.' || $v == '..' || (!$include_hidden && $v[0] == '.')) {
unset($list[$k]);
}
}
return array_values($list);
}
/**
* Return all directories between a path and the specified root directory
* (defaulting to "/"). Iterating over them walks from the path to the root.
*
* @param string $path Path, absolute or relative to PWD.
* @param string $root (optional) The root directory.
* @return list<string> List of parent paths, including the provided path.
* @task directory
*/
public static function walkToRoot($path, $root = null) {
$path = self::resolvePath($path);
if (is_link($path)) {
$path = realpath($path);
}
// NOTE: On Windows, paths start like "C:\", so "/" does not contain
// every other path. We could possibly special case "/" to have the same
// meaning on Windows that it does on Linux, but just special case the
// common case for now. See PHI817.
if ($root !== null) {
$root = self::resolvePath($root);
if (is_link($root)) {
$root = realpath($root);
}
// NOTE: We don't use `isDescendant()` here because we don't want to
// reject paths which don't exist on disk.
$root_list = new FileList(array($root));
if (!$root_list->contains($path)) {
return array();
}
} else {
if (phutil_is_windows()) {
$root = null;
} else {
$root = '/';
}
}
$walk = array();
$parts = explode(DIRECTORY_SEPARATOR, $path);
foreach ($parts as $k => $part) {
if (!strlen($part)) {
unset($parts[$k]);
}
}
while (true) {
if (phutil_is_windows()) {
$next = implode(DIRECTORY_SEPARATOR, $parts);
} else {
$next = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts);
}
$walk[] = $next;
if ($next == $root) {
break;
}
if (!$parts) {
break;
}
array_pop($parts);
}
return $walk;
}
/* -( Paths )-------------------------------------------------------------- */
/**
* Checks if a path is specified as an absolute path.
*
* @param string $path
* @return bool
*/
public static function isAbsolutePath($path) {
if (phutil_is_windows()) {
return (bool)preg_match('/^[A-Za-z]+:/', $path);
} else {
return !strncmp($path, DIRECTORY_SEPARATOR, 1);
}
}
/**
* Canonicalize a path by resolving it relative to some directory (by
* default PWD), following parent symlinks and removing artifacts. If the
* path is itself a symlink it is left unresolved.
*
* @param string $path Path, absolute or relative to PWD.
* @return string $relative_to (optional) Canonical, absolute path.
*
* @task path
*/
public static function resolvePath($path, $relative_to = null) {
$is_absolute = self::isAbsolutePath($path);
if (!$is_absolute) {
if (!$relative_to) {
$relative_to = getcwd();
}
$path = $relative_to.DIRECTORY_SEPARATOR.$path;
}
if (is_link($path)) {
$parent_realpath = realpath(dirname($path));
if ($parent_realpath !== false) {
return $parent_realpath.DIRECTORY_SEPARATOR.basename($path);
}
}
$realpath = realpath($path);
if ($realpath !== false) {
return $realpath;
}
// This won't work if the file doesn't exist or is on an unreadable mount
// or something crazy like that. Try to resolve a parent so we at least
// cover the nonexistent file case.
// We're also normalizing path separators to whatever is normal for the
// environment.
if (phutil_is_windows()) {
$parts = trim($path, '/\\');
$parts = preg_split('([/\\\\])', $parts);
// Normalize the directory separators in the path. If we find a parent
// below, we'll overwrite this with a better resolved path.
$path = str_replace('/', '\\', $path);
} else {
$parts = trim($path, '/');
$parts = explode('/', $parts);
}
while ($parts) {
array_pop($parts);
if (phutil_is_windows()) {
$attempt = implode(DIRECTORY_SEPARATOR, $parts);
} else {
$attempt = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts);
}
$realpath = realpath($attempt);
if ($realpath !== false) {
$path = $realpath.substr($path, strlen($attempt));
break;
}
}
return $path;
}
/**
* Test whether a path is descendant from some root path after resolving all
* symlinks and removing artifacts. Both paths must exists for the relation
* to obtain. A path is always a descendant of itself as long as it exists.
*
* @param string $path Child path, absolute or relative to PWD.
* @param string $root Root path, absolute or relative to PWD.
* @return bool True if resolved child path is in fact a descendant of
* resolved root path and both exist.
* @task path
*/
public static function isDescendant($path, $root) {
try {
self::assertExists($path);
self::assertExists($root);
} catch (FilesystemException $e) {
return false;
}
$fs = new FileList(array($root));
return $fs->contains($path);
}
/**
* Convert a canonical path to its most human-readable format. It is
* guaranteed that you can use resolvePath() to restore a path to its
* canonical format.
*
* @param string $path Path, absolute or relative to PWD.
* @param string $pwd (optional) Working directory to make files readable
* relative to.
* @return string Human-readable path.
*
* @task path
*/
public static function readablePath($path, $pwd = null) {
if ($pwd === null) {
$pwd = getcwd();
}
foreach (array($pwd, self::resolvePath($pwd)) as $parent) {
$parent = rtrim($parent, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
$len = strlen($parent);
if (!strncmp($parent, $path, $len)) {
$path = substr($path, $len);
return $path;
}
}
return $path;
}
/**
* Determine whether or not a path exists in the filesystem. This differs from
* file_exists() in that it returns true for symlinks. This method does not
* attempt to resolve paths before testing them.
*
* @param string $path Test for the existence of this path.
* @return bool True if the path exists in the filesystem.
* @task path
*/
public static function pathExists($path) {
return file_exists($path) || is_link($path);
}
/**
* Determine if an executable binary (like `git` or `svn`) exists within
* the configured `$PATH`.
*
* @param string $binary Binary name, like `'git'` or `'svn'`.
* @return bool True if the binary exists and is executable.
* @task exec
*/
public static function binaryExists($binary) {
return self::resolveBinary($binary) !== null;
}
/**
* Locates the full path that an executable binary (like `git` or `svn`) is at
* the configured `$PATH`.
*
* @param string $binary Binary name, like `'git'` or `'svn'`.
* @return string|null The full binary path if it is present, or null.
* @task exec
*/
public static function resolveBinary($binary) {
if (phutil_is_windows()) {
list($err, $stdout) = exec_manual('where %s', $binary);
$stdout = phutil_split_lines($stdout);
// If `where %s` could not find anything, check for relative binary
if ($err) {
$path = self::resolvePath($binary);
if (self::pathExists($path)) {
return $path;
}
return null;
}
// These are the only file extensions that can be executed directly
// when using proc_open() with 'bypass_shell'.
$executable_extensions = ['exe', 'bat', 'cmd', 'com'];
foreach ($stdout as $line) {
$path = trim($line);
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (in_array($ext, $executable_extensions)) {
return $path;
}
}
return null;
} else {
list($err, $stdout) = exec_manual('which %s', $binary);
return $err === 0 ? trim($stdout) : null;
}
}
/**
* Determine if two paths are equivalent by resolving symlinks. This is
* different from resolving both paths and comparing them because
* resolvePath() only resolves symlinks in parent directories, not the
* path itself.
*
* @param string $u First path to test for equivalence.
* @param string $v Second path to test for equivalence.
* @return bool True if both paths are equivalent, i.e. reference the same
* entity in the filesystem.
* @task path
*/
public static function pathsAreEquivalent($u, $v) {
$u = self::resolvePath($u);
$v = self::resolvePath($v);
$real_u = realpath($u);
$real_v = realpath($v);
if ($real_u) {
$u = $real_u;
}
if ($real_v) {
$v = $real_v;
}
return ($u == $v);
}
public static function concatenatePaths(array $components) {
$components = implode(DIRECTORY_SEPARATOR, $components);
// Replace any extra sequences of directory separators with a single
// separator, so we don't end up with "path//to///thing.c".
$components = preg_replace(
'('.preg_quote(DIRECTORY_SEPARATOR).'{2,})',
DIRECTORY_SEPARATOR,
$components);
return $components;
}
/* -( Assert )------------------------------------------------------------- */
/**
* Assert that something (e.g., a file, directory, or symlink) exists at a
* specified location.
*
* @param string $path Assert that this path exists.
* @return void
*
* @task assert
*/
public static function assertExists($path) {
if (self::pathExists($path)) {
return;
}
// Before we claim that the path doesn't exist, try to find a parent we
// don't have "+x" on. If we find one, tailor the error message so we don't
// say "does not exist" in cases where the path does exist, we just don't
// have permission to test its existence.
foreach (self::walkToRoot($path) as $parent) {
if (!self::pathExists($parent)) {
continue;
}
if (!is_dir($parent)) {
continue;
}
if (phutil_is_windows()) {
// Do nothing. On Windows, there's no obvious equivalent to the
// check below because "is_executable(...)" always appears to return
// "false" for any directory.
} else if (!is_executable($parent)) {
// On Linux, note that we don't need read permission ("+r") on parent
// directories to determine that a path exists, only execute ("+x").
throw new FilesystemException(
$path,
pht(
'Filesystem path "%s" can not be accessed because a parent '.
'directory ("%s") is not executable (the current process does '.
'not have "+x" permission).',
$path,
$parent));
}
}
throw new FilesystemException(
$path,
pht(
'Filesystem path "%s" does not exist.',
$path));
}
/**
* Assert that nothing exists at a specified location.
*
* @param string $path Assert that this path does not exist.
* @return void
*
* @task assert
*/
public static function assertNotExists($path) {
if (file_exists($path) || is_link($path)) {
throw new FilesystemException(
$path,
pht("Path '%s' already exists!", $path));
}
}
/**
* Assert that a path represents a file, strictly (i.e., not a directory).
*
* @param string $path Assert that this path is a file.
* @return void
*
* @task assert
*/
public static function assertIsFile($path) {
if (!is_file($path)) {
throw new FilesystemException(
$path,
pht("Requested path '%s' is not a file.", $path));
}
}
/**
* Assert that a path represents a directory, strictly (i.e., not a file).
*
* @param string $path Assert that this path is a directory.
* @return void
*
* @task assert
*/
public static function assertIsDirectory($path) {
if (!is_dir($path)) {
throw new FilesystemException(
$path,
pht("Requested path '%s' is not a directory.", $path));
}
}
/**
* Assert that a file or directory exists and is writable.
*
* @param string $path Assert that this path is writable.
* @return void
*
* @task assert
*/
public static function assertWritable($path) {
if (!is_writable($path)) {
throw new FilesystemException(
$path,
pht("Requested path '%s' is not writable.", $path));
}
}
/**
* Assert that a file or directory exists and is readable.
*
* @param string $path Assert that this path is readable.
* @return void
*
* @task assert
*/
public static function assertReadable($path) {
if (!is_readable($path)) {
throw new FilesystemException(
$path,
pht("Path '%s' is not readable.", $path));
}
}
}
diff --git a/support/init/init-script.php b/support/init/init-script.php
index 9e3de5245e..96b7d8f6fb 100644
--- a/support/init/init-script.php
+++ b/support/init/init-script.php
@@ -1,127 +1,127 @@
<?php
function __arcanist_init_script__() {
// Adjust the runtime language configuration to be reasonable and inline with
// expectations. We do this first, then load libraries.
// There may be some kind of auto-prepend script configured which starts an
// output buffer. Discard any such output buffers so messages can be sent to
// stdout (if a user wants to capture output from a script, there are a large
// number of ways they can accomplish it legitimately; historically, we ran
// into this on only one install which had some bizarre configuration, but it
// was difficult to diagnose because the symptom is "no messages of any
// kind").
while (ob_get_level() > 0) {
ob_end_clean();
}
error_reporting(E_ALL);
$config_map = array(
// Always display script errors. Without this, they may not appear, which is
// unhelpful when users encounter a problem. On the web this is a security
// concern because you don't want to expose errors to clients, but in a
// script context we always want to show errors.
'display_errors' => true,
// Send script error messages to the server's `error_log` setting.
'log_errors' => true,
// Set the error log to the default, so errors go to stderr. Without this
// errors may end up in some log, and users may not know where the log is
// or check it.
'error_log' => null,
// XDebug raises a fatal error if the call stack gets too deep, but the
// default setting is 100, which we may exceed legitimately with module
// includes (and in other cases, like recursive filesystem operations
// applied to 100+ levels of directory nesting). Stop it from triggering:
// we explicitly limit recursive algorithms which should be limited.
//
// After Feb 2014, XDebug interprets a value of 0 to mean "do not allow any
// function calls". Previously, 0 effectively disabled this check. For
// context, see T5027.
'xdebug.max_nesting_level' => PHP_INT_MAX,
// Don't limit memory, doing so just generally just prevents us from
// processing large inputs without many tangible benefits.
'memory_limit' => -1,
// See PHI1894. This option was introduced in PHP 7.4, and removes the
// "args" value from exception backtraces. We have some unit tests which
// inspect "args", and this option generally obscures useful debugging
// information without any benefit in the context of Phabricator.
'zend.exception_ignore_args' => 0,
// See T13100. We'd like the regex engine to fail, rather than segfault,
// if handed a pathological regular expression.
'pcre.backtrack_limit' => 10000,
'pcre.recusion_limit' => 10000,
// NOTE: Phabricator applies a similar set of startup options for Web
// environments in "PhabricatorStartup". Changes here may also be
// appropriate to apply there.
);
foreach ($config_map as $config_key => $config_value) {
ini_set($config_key, $config_value);
}
$php_version = phpversion();
- $min_version = '5.5.0';
+ $min_version = '7.2.25';
if (version_compare($php_version, $min_version, '<')) {
echo sprintf(
'UPGRADE PHP: '.
'The installed version of PHP ("%s") is too old to run Arcanist. '.
'Update PHP to at least the minimum required version ("%s").',
$php_version,
$min_version);
echo "\n";
exit(1);
}
if (!ini_get('date.timezone')) {
// If the timezone isn't set, PHP issues a warning whenever you try to parse
// a date (like those from Git or Mercurial logs), even if the date contains
// timezone information (like "PST" or "-0700") which makes the
// environmental timezone setting is completely irrelevant. We never rely on
// the system timezone setting in any capacity, so prevent PHP from flipping
// out by setting it to a safe default (UTC) if it isn't set to some other
// value.
date_default_timezone_set('UTC');
}
// Adjust `include_path`.
ini_set('include_path', implode(PATH_SEPARATOR, array(
dirname(dirname(__FILE__)).'/externals/includes',
ini_get('include_path'),
)));
// Disable the insanely dangerous XML entity loader by default.
// PHP 8 deprecates this function and disables this by default; remove once
// PHP 7 is no longer supported or a future version has removed the function
// entirely.
if (function_exists('libxml_disable_entity_loader')) {
@libxml_disable_entity_loader(true);
}
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/src/init/init-library.php';
PhutilErrorHandler::initialize();
// If "variables_order" excludes "E", silently repair it so that $_ENV has
// the values we expect.
PhutilExecutionEnvironment::repairMissingVariablesOrder();
$router = PhutilSignalRouter::initialize();
$handler = new PhutilBacktraceSignalHandler();
$router->installHandler('phutil.backtrace', $handler);
$handler = new PhutilConsoleMetricsSignalHandler();
$router->installHandler('phutil.winch', $handler);
}
__arcanist_init_script__();
File Metadata
详情
附加的
Mime Type
text/x-diff
Expires
9月 9 Tue, 5:45 AM (7 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5355
默认替代文本
(45 KB)
Attached To
Mode
rP phorge
附加的
Detach File
Event Timeline
Log In to Comment