1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-21 22:32:41 +01:00

Fully merge "libphutil/" into "arcanist/"

Summary: Ref T13395. Moves all remaining code in "libphutil/" into "arcanist/".

Test Plan: Ran various arc workflows, although this probably has some remaining rough edges.

Maniphest Tasks: T13395

Differential Revision: https://secure.phabricator.com/D20980
This commit is contained in:
epriestley 2020-02-12 14:24:11 -08:00
parent a36e60f0a3
commit 9b74cb4ee6
499 changed files with 179518 additions and 70 deletions

View file

@ -36,7 +36,19 @@
"exclude": "(^resources/spelling/.*\\.json$)" "exclude": "(^resources/spelling/.*\\.json$)"
}, },
"text": { "text": {
"type": "text" "type": "text",
"exclude": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))"
]
},
"text-without-length": {
"type": "text",
"include": [
"(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json|expect))"
],
"severity": {
"3": "disabled"
}
}, },
"xhpast": { "xhpast": {
"type": "xhpast", "type": "xhpast",

19
.gitignore vendored
View file

@ -11,3 +11,22 @@
# User extensions # User extensions
/externals/includes/* /externals/includes/*
/src/extensions/* /src/extensions/*
# XHPAST
/support/xhpast/*.a
/support/xhpast/*.o
/support/xhpast/parser.yacc.output
/support/xhpast/node_names.hpp
/support/xhpast/xhpast
/support/xhpast/xhpast.exe
/src/parser/xhpast/bin/xhpast
## NOTE: Don't .gitignore these files! Even though they're build artifacts, we
## want to check them in so users can build xhpast without flex/bison.
# /support/xhpast/parser.yacc.cpp
# /support/xhpast/parser.yacc.hpp
# /support/xhpast/scanner.lex.cpp
# /support/xhpast/scanner.lex.hpp
# This is an OS X build artifact.
/support/xhpast/xhpast.dSYM

19
externals/jsonlint/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2011 Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,488 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Parser class
*
* Example:
*
* $parser = new JsonParser();
* // returns null if it's valid json, or an error object
* $parser->lint($json);
* // returns parsed json, like json_decode does, but slower, throws exceptions on failure.
* $parser->parse($json);
*
* Ported from https://github.com/zaach/jsonlint
*/
class JsonLintJsonParser
{
const DETECT_KEY_CONFLICTS = 1;
const ALLOW_DUPLICATE_KEYS = 2;
const PARSE_TO_ASSOC = 4;
private $lexer;
private $flags;
private $stack;
private $vstack; // semantic value stack
private $lstack; // location stack
private $symbols = array(
'error' => 2,
'JSONString' => 3,
'STRING' => 4,
'JSONNumber' => 5,
'NUMBER' => 6,
'JSONNullLiteral' => 7,
'NULL' => 8,
'JSONBooleanLiteral' => 9,
'TRUE' => 10,
'FALSE' => 11,
'JSONText' => 12,
'JSONValue' => 13,
'EOF' => 14,
'JSONObject' => 15,
'JSONArray' => 16,
'{' => 17,
'}' => 18,
'JSONMemberList' => 19,
'JSONMember' => 20,
':' => 21,
',' => 22,
'[' => 23,
']' => 24,
'JSONElementList' => 25,
'$accept' => 0,
'$end' => 1,
);
private $terminals_ = array(
2 => "error",
4 => "STRING",
6 => "NUMBER",
8 => "NULL",
10 => "TRUE",
11 => "FALSE",
14 => "EOF",
17 => "{",
18 => "}",
21 => ":",
22 => ",",
23 => "[",
24 => "]",
);
private $productions_ = array(
0,
array(3, 1),
array(5, 1),
array(7, 1),
array(9, 1),
array(9, 1),
array(12, 2),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(13, 1),
array(15, 2),
array(15, 3),
array(20, 3),
array(19, 1),
array(19, 3),
array(16, 2),
array(16, 3),
array(25, 1),
array(25, 3)
);
private $table = array(array(3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 1 => array(3)), array( 14 => array(1,16)), array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), array( 1 => array(2,6)), array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), array( 18 => array(1,24), 22 => array(1,25)), array( 18 => array(2,16), 22 => array(2,16)), array( 21 => array(1,26)), array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), array( 22 => array(1,28), 24 => array(1,27)), array( 22 => array(2,20), 24 => array(2,20)), array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), array( 3 => 20, 4 => array(1,12), 20 => 29 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 18 => array(2,17), 22 => array(2,17)), array( 18 => array(2,15), 22 => array(2,15)), array( 22 => array(2,21), 24 => array(2,21)),
);
private $defaultActions = array(
16 => array(2, 6)
);
/**
* @param string $input JSON string
* @return null|JsonLintParsingException null if no error is found, a JsonLintParsingException containing all details otherwise
*/
public function lint($input)
{
try {
$this->parse($input);
} catch (JsonLintParsingException $e) {
return $e;
}
}
/**
* @param string $input JSON string
* @return mixed
* @throws JsonLintParsingException
*/
public function parse($input, $flags = 0)
{
$this->failOnBOM($input);
$this->flags = $flags;
$this->stack = array(0);
$this->vstack = array(null);
$this->lstack = array();
$yytext = '';
$yylineno = 0;
$yyleng = 0;
$recovering = 0;
$TERROR = 2;
$EOF = 1;
$this->lexer = new JsonLintLexer();
$this->lexer->setInput($input);
$yyloc = $this->lexer->yylloc;
$this->lstack[] = $yyloc;
$symbol = null;
$preErrorSymbol = null;
$state = null;
$action = null;
$a = null;
$r = null;
$yyval = new stdClass;
$p = null;
$len = null;
$newState = null;
$expected = null;
$errStr = null;
while (true) {
// retrieve state number from top of stack
$state = $this->stack[count($this->stack)-1];
// use default actions if available
if (isset($this->defaultActions[$state])) {
$action = $this->defaultActions[$state];
} else {
if ($symbol == null) {
$symbol = $this->lex();
}
// read action for current state and first input
$action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false;
}
// handle parse error
if (!$action || !$action[0]) {
if (!$recovering) {
// Report error
$expected = array();
foreach ($this->table[$state] as $p => $ignore) {
if (isset($this->terminals_[$p]) && $p > 2) {
$expected[] = "'" . $this->terminals_[$p] . "'";
}
}
$message = null;
if (in_array("'STRING'", $expected) && in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) {
$message = "Invalid string";
if ("'" === substr($this->lexer->match, 0, 1)) {
$message .= ", it appears you used single quotes instead of double quotes";
} elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u])}', $this->lexer->getUpcomingInput(), $match)) {
$message .= ", it appears you have an unescaped backslash at: ".$match[1];
} elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getUpcomingInput())) {
$message .= ", it appears you forgot to terminate the string, or attempted to write a multiline string which is invalid";
}
}
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
if ($message) {
$errStr .= $message;
} else {
$errStr .= (count($expected) > 1) ? "Expected one of: " : "Expected: ";
$errStr .= implode(', ', $expected);
}
if (',' === substr(trim($this->lexer->getPastInput()), -1)) {
$errStr .= " - It appears you have an extra trailing comma";
}
$this->parseError($errStr, array(
'text' => $this->lexer->match,
'token' => !empty($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol,
'line' => $this->lexer->yylineno,
'loc' => $yyloc,
'expected' => $expected,
));
}
// just recovered from another error
if ($recovering == 3) {
if ($symbol == $EOF) {
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
}
// discard current lookahead and grab another
$yyleng = $this->lexer->yyleng;
$yytext = $this->lexer->yytext;
$yylineno = $this->lexer->yylineno;
$yyloc = $this->lexer->yylloc;
$symbol = $this->lex();
}
// try to recover from error
while (true) {
// check for error recovery rule in this state
if (array_key_exists($TERROR, $this->table[$state])) {
break;
}
if ($state == 0) {
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
}
$this->popStack(1);
$state = $this->stack[count($this->stack)-1];
}
$preErrorSymbol = $symbol; // save the lookahead token
$symbol = $TERROR; // insert generic error symbol as new lookahead
$state = $this->stack[count($this->stack)-1];
$action = isset($this->table[$state][$TERROR]) ? $this->table[$state][$TERROR] : false;
$recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
}
// this shouldn't happen, unless resolve defaults are off
if (is_array($action[0]) && count($action) > 1) {
throw new JsonLintParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol);
}
switch ($action[0]) {
case 1: // shift
$this->stack[] = $symbol;
$this->vstack[] = $this->lexer->yytext;
$this->lstack[] = $this->lexer->yylloc;
$this->stack[] = $action[1]; // push state
$symbol = null;
if (!$preErrorSymbol) { // normal execution/no error
$yyleng = $this->lexer->yyleng;
$yytext = $this->lexer->yytext;
$yylineno = $this->lexer->yylineno;
$yyloc = $this->lexer->yylloc;
if ($recovering > 0) {
$recovering--;
}
} else { // error just occurred, resume old lookahead f/ before error
$symbol = $preErrorSymbol;
$preErrorSymbol = null;
}
break;
case 2: // reduce
$len = $this->productions_[$action[1]][1];
// perform semantic action
$yyval->token = $this->vstack[count($this->vstack) - $len]; // default to $$ = $1
// default location, uses first token for firsts, last for lasts
$yyval->store = array( // _$ = store
'first_line' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_line'],
'last_line' => $this->lstack[count($this->lstack) - 1]['last_line'],
'first_column' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_column'],
'last_column' => $this->lstack[count($this->lstack) - 1]['last_column'],
);
$r = $this->performAction($yyval, $yytext, $yyleng, $yylineno, $action[1], $this->vstack, $this->lstack);
if (!$r instanceof JsonLintUndefined) {
return $r;
}
if ($len) {
$this->popStack($len);
}
$this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce)
$this->vstack[] = $yyval->token;
$this->lstack[] = $yyval->store;
$newState = $this->table[$this->stack[count($this->stack)-2]][$this->stack[count($this->stack)-1]];
$this->stack[] = $newState;
break;
case 3: // accept
return true;
}
}
return true;
}
protected function parseError($str, $hash)
{
throw new JsonLintParsingException($str, $hash);
}
// $$ = $tokens // needs to be passed by ref?
// $ = $token
// _$ removed, useless?
private function performAction(stdClass $yyval, $yytext, $yyleng, $yylineno, $yystate, &$tokens)
{
// $0 = $len
$len = count($tokens) - 1;
switch ($yystate) {
case 1:
$yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext);
$yyval->token = $yytext;
break;
case 2:
if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) {
$yyval->token = floatval($yytext);
} else {
$yyval->token = strpos($yytext, '.') === false ? intval($yytext) : floatval($yytext);
}
break;
case 3:
$yyval->token = null;
break;
case 4:
$yyval->token = true;
break;
case 5:
$yyval->token = false;
break;
case 6:
return $yyval->token = $tokens[$len-1];
case 13:
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token = array();
} else {
$yyval->token = new stdClass;
}
break;
case 14:
$yyval->token = $tokens[$len-1];
break;
case 15:
$yyval->token = array($tokens[$len-2], $tokens[$len]);
break;
case 16:
$property = $tokens[$len][0];
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token = array();
$yyval->token[$property] = $tokens[$len][1];
} else {
$yyval->token = new stdClass;
$yyval->token->$property = $tokens[$len][1];
}
break;
case 17:
if ($this->flags & self::PARSE_TO_ASSOC) {
$yyval->token =& $tokens[$len-2];
$key = $tokens[$len][0];
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2][$key])) {
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$tokens[$len][0];
throw new JsonLintParsingException($errStr);
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2][$key])) {
// Forget about it...
}
$tokens[$len-2][$key] = $tokens[$len][1];
} else {
$yyval->token = $tokens[$len-2];
$key = $tokens[$len][0];
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2]->{$key})) {
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
$errStr .= $this->lexer->showPosition() . "\n";
$errStr .= "Duplicate key: ".$tokens[$len][0];
throw new JsonLintParsingException($errStr);
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2]->{$key})) {
$duplicateCount = 1;
do {
$duplicateKey = $key . '.' . $duplicateCount++;
} while (isset($tokens[$len-2]->$duplicateKey));
$key = $duplicateKey;
}
$tokens[$len-2]->$key = $tokens[$len][1];
}
break;
case 18:
$yyval->token = array();
break;
case 19:
$yyval->token = $tokens[$len-1];
break;
case 20:
$yyval->token = array($tokens[$len]);
break;
case 21:
$tokens[$len-2][] = $tokens[$len];
$yyval->token = $tokens[$len-2];
break;
}
return new JsonLintUndefined();
}
private function stringInterpolation($match)
{
switch ($match[0]) {
case '\\\\':
return '\\';
case '\"':
return '"';
case '\b':
return chr(8);
case '\f':
return chr(12);
case '\n':
return "\n";
case '\r':
return "\r";
case '\t':
return "\t";
case '\/':
return "/";
default:
return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', 0, 'UTF-8');
}
}
private function popStack($n)
{
$this->stack = array_slice($this->stack, 0, - (2 * $n));
$this->vstack = array_slice($this->vstack, 0, - $n);
$this->lstack = array_slice($this->lstack, 0, - $n);
}
private function lex()
{
$token = $this->lexer->lex();
if (!$token) {
$token = 1;
}
// if token isn't its numeric value, convert
if (!is_numeric($token)) {
$token = isset($this->symbols[$token]) ? $this->symbols[$token] : $token;
}
return $token;
}
private function failOnBOM($input)
{
// UTF-8 ByteOrderMark sequence
$bom = "\xEF\xBB\xBF";
if (substr($input, 0, 3) === $bom) {
$this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark", array());
}
}
}

View file

@ -0,0 +1,215 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Lexer class
*
* Ported from https://github.com/zaach/jsonlint
*/
class JsonLintLexer
{
private $EOF = 1;
private $rules = array(
0 => '/^\s+/',
1 => '/^-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/',
2 => '{^"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}',
3 => '/^\{/',
4 => '/^\}/',
5 => '/^\[/',
6 => '/^\]/',
7 => '/^,/',
8 => '/^:/',
9 => '/^true\b/',
10 => '/^false\b/',
11 => '/^null\b/',
12 => '/^$/',
13 => '/^./',
);
private $conditions = array(
"INITIAL" => array(
"rules" => array(0,1,2,3,4,5,6,7,8,9,10,11,12,13),
"inclusive" => true,
),
);
private $conditionStack;
private $input;
private $more;
private $done;
private $matched;
public $match;
public $yylineno;
public $yyleng;
public $yytext;
public $yylloc;
public function lex()
{
$r = $this->next();
if (!$r instanceof JsonLintUndefined) {
return $r;
}
return $this->lex();
}
public function setInput($input)
{
$this->input = $input;
$this->more = false;
$this->done = false;
$this->yylineno = $this->yyleng = 0;
$this->yytext = $this->matched = $this->match = '';
$this->conditionStack = array('INITIAL');
$this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0);
return $this;
}
public function showPosition()
{
$pre = str_replace("\n", '', $this->getPastInput());
$c = str_repeat('-', max(0, strlen($pre) - 1)); // new Array(pre.length + 1).join("-");
return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^";
}
public function getPastInput()
{
$past = substr($this->matched, 0, strlen($this->matched) - strlen($this->match));
return (strlen($past) > 20 ? '...' : '') . substr($past, -20);
}
public function getUpcomingInput()
{
$next = $this->match;
if (strlen($next) < 20) {
$next .= substr($this->input, 0, 20 - strlen($next));
}
return substr($next, 0, 20) . (strlen($next) > 20 ? '...' : '');
}
protected function parseError($str, $hash)
{
throw new Exception($str);
}
private function next()
{
if ($this->done) {
return $this->EOF;
}
if (!$this->input) {
$this->done = true;
}
$token = null;
$match = null;
$col = null;
$lines = null;
if (!$this->more) {
$this->yytext = '';
$this->match = '';
}
$rules = $this->getCurrentRules();
$rulesLen = count($rules);
for ($i=0; $i < $rulesLen; $i++) {
if (preg_match($this->rules[$rules[$i]], $this->input, $match)) {
preg_match_all('/\n.*/', $match[0], $lines);
$lines = $lines[0];
if ($lines) {
$this->yylineno += count($lines);
}
$this->yylloc = array(
'first_line' => $this->yylloc['last_line'],
'last_line' => $this->yylineno+1,
'first_column' => $this->yylloc['last_column'],
'last_column' => $lines ? strlen($lines[count($lines) - 1]) - 1 : $this->yylloc['last_column'] + strlen($match[0]),
);
$this->yytext .= $match[0];
$this->match .= $match[0];
$this->yyleng = strlen($this->yytext);
$this->more = false;
$this->input = substr($this->input, strlen($match[0]));
$this->matched .= $match[0];
$token = $this->performAction($rules[$i], $this->conditionStack[count($this->conditionStack)-1]);
if ($token) {
return $token;
}
return new JsonLintUndefined();
}
}
if ($this->input === "") {
return $this->EOF;
}
$this->parseError(
'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition(),
array(
'text' => "",
'token' => null,
'line' => $this->yylineno,
)
);
}
private function getCurrentRules()
{
return $this->conditions[$this->conditionStack[count($this->conditionStack)-1]]['rules'];
}
private function performAction($avoiding_name_collisions, $YY_START)
{
switch ($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:
return 6;
break;
case 2:
$this->yytext = substr($this->yytext, 1, $this->yyleng-2);
return 4;
case 3:
return 17;
case 4:
return 18;
case 5:
return 23;
case 6:
return 24;
case 7:
return 22;
case 8:
return 21;
case 9:
return 10;
case 10:
return 11;
case 11:
return 8;
case 12:
return 14;
case 13:
return 'INVALID';
}
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class JsonLintParsingException extends Exception
{
protected $details;
public function __construct($message, $details = array())
{
$this->details = $details;
parent::__construct($message);
}
public function getDetails()
{
return $this->details;
}
}

View file

@ -0,0 +1,14 @@
<?php
/*
* This file is part of the JSON Lint package.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
class JsonLintUndefined
{
}

File diff suppressed because it is too large Load diff

45
resources/ssl/README Normal file
View file

@ -0,0 +1,45 @@
This document describes how to set Certificate Authority information.
Usually, you need to do this only if you're using a self-signed certificate.
OSX after Yosemite
==================
If you're using a version of Mac OSX after Yosemite, you can not configure
certificates from the command line. All libphutil and arcanist options
related to CA configuration are ignored.
Instead, you need to add them to the system keychain. The easiest way to do this
is to visit the site in Safari and choose to permanently accept the certificate.
You can also use `security add-trusted-cert` from the command line.
All Other Systems
=================
If "curl.cainfo" is not set (or you are using PHP older than 5.3.7, where the
option was introduced), libphutil uses the "default.pem" certificate authority
bundle when making HTTPS requests with cURL. This bundle is extracted from
Mozilla's certificates by cURL:
http://curl.haxx.se/docs/caextract.html
If you want to use a different CA bundle (for example, because you use
self-signed certificates), set "curl.cainfo" if you're using PHP 5.3.7 or newer,
or create a file (or symlink) in this directory named "custom.pem".
If "custom.pem" is present, that file will be used instead of "default.pem".
If you receive errors using your "custom.pem" file, you can test it directly
with `curl` by running a command like this:
curl -v --cacert path/to/your/custom.pem https://phabricator.example.com/
Replace "path/to/your/custom.pem" with the path to your "custom.pem" file,
and replace "https://phabricator.example.com" with the real URL of your
Phabricator install.
The initial lines of output from `curl` should give you information about the
SSL handshake and certificate verification, which may be helpful in resolving
the issue.

3893
resources/ssl/default.pem Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,58 +1,3 @@
<?php <?php
/** require_once dirname(dirname(__FILE__)).'/scripts/init/init-script.php';
* Adjust 'include_path' to add locations where we'll search for libphutil.
* We look in these places:
*
* - Next to 'arcanist/'.
* - Anywhere in the normal PHP 'include_path'.
* - Inside 'arcanist/externals/includes/'.
*
* When looking in these places, we expect to find a 'libphutil/' directory.
*/
function arcanist_adjust_php_include_path() {
// The 'arcanist/' directory.
$arcanist_dir = dirname(dirname(__FILE__));
// The parent directory of 'arcanist/'.
$parent_dir = dirname($arcanist_dir);
// The 'arcanist/externals/includes/' directory.
$include_dir = implode(
DIRECTORY_SEPARATOR,
array(
$arcanist_dir,
'externals',
'includes',
));
$php_include_path = ini_get('include_path');
$php_include_path = implode(
PATH_SEPARATOR,
array(
$parent_dir,
$php_include_path,
$include_dir,
));
ini_set('include_path', $php_include_path);
}
arcanist_adjust_php_include_path();
if (getenv('ARC_PHUTIL_PATH')) {
@include_once getenv('ARC_PHUTIL_PATH').'/scripts/__init_script__.php';
} else {
@include_once 'libphutil/scripts/__init_script__.php';
}
if (!@constant('__LIBPHUTIL__')) {
echo "ERROR: Unable to load libphutil. Put libphutil/ next to arcanist/, or ".
"update your PHP 'include_path' to include the parent directory of ".
"libphutil/, or symlink libphutil/ into arcanist/externals/includes/.\n";
exit(1);
}
phutil_load_library(dirname(dirname(__FILE__)).'/src/');
PhutilTranslator::getInstance()
->setLocale(PhutilLocale::loadLocale('en_US'))
->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US'));

View file

@ -81,7 +81,6 @@ try {
csprintf('%Ls', $original_argv)); csprintf('%Ls', $original_argv));
$libraries = array( $libraries = array(
'phutil',
'arcanist', 'arcanist',
); );
@ -621,7 +620,7 @@ function arcanist_load_libraries(
$error = null; $error = null;
try { try {
phutil_load_library($location); require_once $location.'/__phutil_library_init__.php';
} catch (PhutilBootloaderException $ex) { } catch (PhutilBootloaderException $ex) {
$error = pht( $error = pht(
"Failed to load phutil library at location '%s'. This library ". "Failed to load phutil library at location '%s'. This library ".

View file

@ -0,0 +1,100 @@
<?php
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
} else {
declare(ticks = 1);
}
function __phutil_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 | E_STRICT);
$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 T13296. On macOS under PHP 7.3.x, PCRE currently segfaults after
// "fork()" if "pcre.jit" is enabled.
'pcre.jit' => 0,
);
foreach ($config_map as $config_key => $config_value) {
ini_set($config_key, $config_value);
}
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.
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();
$router = PhutilSignalRouter::initialize();
$handler = new PhutilBacktraceSignalHandler();
$router->installHandler('phutil.backtrace', $handler);
$handler = new PhutilConsoleMetricsSignalHandler();
$router->installHandler('phutil.winch', $handler);
}
__phutil_init_script__();

View file

@ -1,3 +1,3 @@
<?php <?php
phutil_register_library('arcanist', __FILE__); require_once dirname(__FILE__).'/init/init-library.php';

View file

@ -9,6 +9,12 @@
phutil_register_library_map(array( phutil_register_library_map(array(
'__library_version__' => 2, '__library_version__' => 2,
'class' => array( 'class' => array(
'AASTNode' => 'parser/aast/api/AASTNode.php',
'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
'AASTToken' => 'parser/aast/api/AASTToken.php',
'AASTTree' => 'parser/aast/api/AASTTree.php',
'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractMethodBodyXHPASTLinterRule.php', 'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractMethodBodyXHPASTLinterRule.php',
'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase.php', 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase.php',
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistAbstractPrivateMethodXHPASTLinterRule.php',
@ -418,22 +424,466 @@ phutil_register_library_map(array(
'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php', 'ArcanistXMLLinter' => 'lint/linter/ArcanistXMLLinter.php',
'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php', 'ArcanistXMLLinterTestCase' => 'lint/linter/__tests__/ArcanistXMLLinterTestCase.php',
'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php', 'ArcanistXUnitTestResultParser' => 'unit/parser/ArcanistXUnitTestResultParser.php',
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php', 'CSharpToolsTestEngine' => 'unit/engine/CSharpToolsTestEngine.php',
'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
'CommandException' => 'future/exec/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
'ConduitFuture' => 'conduit/ConduitFuture.php',
'ExecFuture' => 'future/exec/ExecFuture.php',
'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
'FileFinder' => 'filesystem/FileFinder.php',
'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
'FileList' => 'filesystem/FileList.php',
'Filesystem' => 'filesystem/Filesystem.php',
'FilesystemException' => 'filesystem/FilesystemException.php',
'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
'Future' => 'future/Future.php',
'FutureIterator' => 'future/FutureIterator.php',
'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
'FutureProxy' => 'future/FutureProxy.php',
'HTTPFuture' => 'future/http/HTTPFuture.php',
'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
'HTTPSFuture' => 'future/http/HTTPSFuture.php',
'ImmediateFuture' => 'future/ImmediateFuture.php',
'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php',
'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php', 'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
'PhageAction' => 'phage/action/PhageAction.php',
'PhageAgentAction' => 'phage/action/PhageAgentAction.php',
'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php',
'PhageLocalAction' => 'phage/action/PhageLocalAction.php',
'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
'PhagePlanAction' => 'phage/action/PhagePlanAction.php',
'Phobject' => 'object/Phobject.php',
'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php', 'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php', 'PhpunitTestEngineTestCase' => 'unit/engine/__tests__/PhpunitTestEngineTestCase.php',
'PhutilAWSCloudFormationFuture' => 'future/aws/PhutilAWSCloudFormationFuture.php',
'PhutilAWSCloudWatchFuture' => 'future/aws/PhutilAWSCloudWatchFuture.php',
'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php',
'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php',
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
'PhutilArray' => 'utils/PhutilArray.php',
'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php',
'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
'PhutilBinaryAnalyzer' => 'filesystem/binary/PhutilBinaryAnalyzer.php',
'PhutilBinaryAnalyzerTestCase' => 'filesystem/binary/__tests__/PhutilBinaryAnalyzerTestCase.php',
'PhutilBootloader' => 'init/lib/PhutilBootloader.php',
'PhutilBootloaderException' => 'init/lib/PhutilBootloaderException.php',
'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
'PhutilCloudWatchMetric' => 'future/aws/PhutilCloudWatchMetric.php',
'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
'PhutilConsole' => 'console/PhutilConsole.php',
'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
'PhutilConsoleError' => 'console/view/PhutilConsoleError.php',
'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
'PhutilConsoleInfo' => 'console/view/PhutilConsoleInfo.php',
'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
'PhutilConsoleLogLine' => 'console/view/PhutilConsoleLogLine.php',
'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
'PhutilConsoleMetrics' => 'console/PhutilConsoleMetrics.php',
'PhutilConsoleMetricsSignalHandler' => 'future/exec/PhutilConsoleMetricsSignalHandler.php',
'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
'PhutilConsoleProgressSink' => 'progress/PhutilConsoleProgressSink.php',
'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
'PhutilConsoleSkip' => 'console/view/PhutilConsoleSkip.php',
'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
'PhutilConsoleWarning' => 'console/view/PhutilConsoleWarning.php',
'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
'PhutilCowsay' => 'utils/PhutilCowsay.php',
'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
'PhutilDOMNode' => 'parser/html/PhutilDOMNode.php',
'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
'PhutilDiffBinaryAnalyzer' => 'filesystem/binary/PhutilDiffBinaryAnalyzer.php',
'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php',
'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php',
'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
'PhutilEvent' => 'events/PhutilEvent.php',
'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
'PhutilEventEngine' => 'events/PhutilEventEngine.php',
'PhutilEventListener' => 'events/PhutilEventListener.php',
'PhutilEventType' => 'events/constant/PhutilEventType.php',
'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php',
'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php',
'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php',
'PhutilGitBinaryAnalyzer' => 'filesystem/binary/PhutilGitBinaryAnalyzer.php',
'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php',
'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php',
'PhutilGitURI' => 'parser/PhutilGitURI.php',
'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
'PhutilHTMLParser' => 'parser/html/PhutilHTMLParser.php',
'PhutilHTMLParserTestCase' => 'parser/html/__tests__/PhutilHTMLParserTestCase.php',
'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
'PhutilHTTPResponse' => 'parser/http/PhutilHTTPResponse.php',
'PhutilHTTPResponseParser' => 'parser/http/PhutilHTTPResponseParser.php',
'PhutilHTTPResponseParserTestCase' => 'parser/http/__tests__/PhutilHTTPResponseParserTestCase.php',
'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php',
'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php',
'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php',
'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
'PhutilJSON' => 'parser/PhutilJSON.php',
'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
'PhutilJavaFragmentLexer' => 'lexer/PhutilJavaFragmentLexer.php',
'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
'PhutilLexer' => 'lexer/PhutilLexer.php',
'PhutilLibraryConflictException' => 'init/lib/PhutilLibraryConflictException.php',
'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
'PhutilLocale' => 'internationalization/PhutilLocale.php',
'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
'PhutilLock' => 'filesystem/PhutilLock.php',
'PhutilLockException' => 'filesystem/PhutilLockException.php',
'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
'PhutilMercurialBinaryAnalyzer' => 'filesystem/binary/PhutilMercurialBinaryAnalyzer.php',
'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
'PhutilMissingSymbolException' => 'init/lib/PhutilMissingSymbolException.php',
'PhutilModuleUtilsTestCase' => 'init/lib/__tests__/PhutilModuleUtilsTestCase.php',
'PhutilNumber' => 'internationalization/PhutilNumber.php',
'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
'PhutilPerson' => 'internationalization/PhutilPerson.php',
'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php',
'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php',
'PhutilPostmarkFuture' => 'future/postmark/PhutilPostmarkFuture.php',
'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
'PhutilProcessQuery' => 'filesystem/PhutilProcessQuery.php',
'PhutilProcessRef' => 'filesystem/PhutilProcessRef.php',
'PhutilProcessRefTestCase' => 'filesystem/__tests__/PhutilProcessRefTestCase.php',
'PhutilProgressSink' => 'progress/PhutilProgressSink.php',
'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
'PhutilProxyException' => 'error/PhutilProxyException.php',
'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php',
'PhutilPygmentizeBinaryAnalyzer' => 'filesystem/binary/PhutilPygmentizeBinaryAnalyzer.php',
'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
'PhutilRope' => 'utils/PhutilRope.php',
'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php',
'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php',
'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php',
'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php',
'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
'PhutilSortVector' => 'utils/PhutilSortVector.php',
'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php',
'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php',
'PhutilSubversionBinaryAnalyzer' => 'filesystem/binary/PhutilSubversionBinaryAnalyzer.php',
'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
'PhutilSystem' => 'utils/PhutilSystem.php',
'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
'PhutilTestCase' => 'unit/engine/phutil/PhutilTestCase.php', 'PhutilTestCase' => 'unit/engine/phutil/PhutilTestCase.php',
'PhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/PhutilTestCaseTestCase.php', 'PhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/PhutilTestCaseTestCase.php',
'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
'PhutilTestSkippedException' => 'unit/engine/phutil/testcase/PhutilTestSkippedException.php', 'PhutilTestSkippedException' => 'unit/engine/phutil/testcase/PhutilTestSkippedException.php',
'PhutilTestTerminatedException' => 'unit/engine/phutil/testcase/PhutilTestTerminatedException.php', 'PhutilTestTerminatedException' => 'unit/engine/phutil/testcase/PhutilTestTerminatedException.php',
'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php',
'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
'PhutilURI' => 'parser/PhutilURI.php',
'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php', 'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php', 'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
'PytestTestEngine' => 'unit/engine/PytestTestEngine.php', 'PytestTestEngine' => 'unit/engine/PytestTestEngine.php',
'TempFile' => 'filesystem/TempFile.php',
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php', 'XUnitTestEngine' => 'unit/engine/XUnitTestEngine.php',
'XUnitTestResultParserTestCase' => 'unit/parser/__tests__/XUnitTestResultParserTestCase.php', 'XUnitTestResultParserTestCase' => 'unit/parser/__tests__/XUnitTestResultParserTestCase.php',
'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
),
'function' => array(
'__phutil_autoload' => 'init/init-library.php',
'array_fuse' => 'utils/utils.php',
'array_interleave' => 'utils/utils.php',
'array_mergev' => 'utils/utils.php',
'array_select_keys' => 'utils/utils.php',
'assert_instances_of' => 'utils/utils.php',
'assert_same_keys' => 'utils/utils.php',
'assert_stringlike' => 'utils/utils.php',
'coalesce' => 'utils/utils.php',
'csprintf' => 'xsprintf/csprintf.php',
'exec_manual' => 'future/exec/execx.php',
'execx' => 'future/exec/execx.php',
'head' => 'utils/utils.php',
'head_key' => 'utils/utils.php',
'hgsprintf' => 'xsprintf/hgsprintf.php',
'id' => 'utils/utils.php',
'idx' => 'utils/utils.php',
'idxv' => 'utils/utils.php',
'ifilter' => 'utils/utils.php',
'igroup' => 'utils/utils.php',
'ipull' => 'utils/utils.php',
'isort' => 'utils/utils.php',
'jsprintf' => 'xsprintf/jsprintf.php',
'last' => 'utils/utils.php',
'last_key' => 'utils/utils.php',
'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
'mfilter' => 'utils/utils.php',
'mgroup' => 'utils/utils.php',
'mpull' => 'utils/utils.php',
'msort' => 'utils/utils.php',
'msortv' => 'utils/utils.php',
'newv' => 'utils/utils.php',
'nonempty' => 'utils/utils.php',
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'phutil_build_http_querystring' => 'utils/utils.php',
'phutil_build_http_querystring_from_pairs' => 'utils/utils.php',
'phutil_censor_credentials' => 'utils/utils.php',
'phutil_console_confirm' => 'console/format.php',
'phutil_console_format' => 'console/format.php',
'phutil_console_get_terminal_width' => 'console/format.php',
'phutil_console_prompt' => 'console/format.php',
'phutil_console_require_tty' => 'console/format.php',
'phutil_console_select' => 'console/format.php',
'phutil_console_wrap' => 'console/format.php',
'phutil_count' => 'internationalization/pht.php',
'phutil_date_format' => 'utils/viewutils.php',
'phutil_decode_mime_header' => 'utils/utils.php',
'phutil_deprecated' => 'init/lib/moduleutils.php',
'phutil_describe_type' => 'utils/utils.php',
'phutil_error_listener_example' => 'error/phlog.php',
'phutil_escape_uri' => 'utils/utils.php',
'phutil_escape_uri_path_component' => 'utils/utils.php',
'phutil_fnmatch' => 'utils/utils.php',
'phutil_format_bytes' => 'utils/viewutils.php',
'phutil_format_relative_time' => 'utils/viewutils.php',
'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
'phutil_format_units_generic' => 'utils/viewutils.php',
'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
'phutil_get_current_library_name' => 'init/lib/moduleutils.php',
'phutil_get_library_name_for_root' => 'init/lib/moduleutils.php',
'phutil_get_library_root' => 'init/lib/moduleutils.php',
'phutil_get_library_root_for_path' => 'init/lib/moduleutils.php',
'phutil_get_signal_name' => 'future/exec/execx.php',
'phutil_get_system_locale' => 'utils/utf8.php',
'phutil_hashes_are_identical' => 'utils/utils.php',
'phutil_http_parameter_pair' => 'utils/utils.php',
'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php',
'phutil_is_natural_list' => 'utils/utils.php',
'phutil_is_system_locale_available' => 'utils/utf8.php',
'phutil_is_utf8' => 'utils/utf8.php',
'phutil_is_utf8_slowly' => 'utils/utf8.php',
'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
'phutil_is_windows' => 'utils/utils.php',
'phutil_json_decode' => 'utils/utils.php',
'phutil_json_encode' => 'utils/utils.php',
'phutil_load_library' => 'init/lib/moduleutils.php',
'phutil_loggable_string' => 'utils/utils.php',
'phutil_microseconds_since' => 'utils/utils.php',
'phutil_parse_bytes' => 'utils/viewutils.php',
'phutil_passthru' => 'future/exec/execx.php',
'phutil_person' => 'internationalization/pht.php',
'phutil_register_library' => 'init/lib/core.php',
'phutil_register_library_map' => 'init/lib/core.php',
'phutil_set_system_locale' => 'utils/utf8.php',
'phutil_split_lines' => 'utils/utils.php',
'phutil_string_cast' => 'utils/utils.php',
'phutil_unescape_uri_path_component' => 'utils/utils.php',
'phutil_units' => 'utils/utils.php',
'phutil_utf8_console_strlen' => 'utils/utf8.php',
'phutil_utf8_convert' => 'utils/utf8.php',
'phutil_utf8_encode_codepoint' => 'utils/utf8.php',
'phutil_utf8_hard_wrap' => 'utils/utf8.php',
'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
'phutil_utf8_is_cjk' => 'utils/utf8.php',
'phutil_utf8_is_combining_character' => 'utils/utf8.php',
'phutil_utf8_strlen' => 'utils/utf8.php',
'phutil_utf8_strtolower' => 'utils/utf8.php',
'phutil_utf8_strtoupper' => 'utils/utf8.php',
'phutil_utf8_strtr' => 'utils/utf8.php',
'phutil_utf8_ucwords' => 'utils/utf8.php',
'phutil_utf8ize' => 'utils/utf8.php',
'phutil_utf8v' => 'utils/utf8.php',
'phutil_utf8v_codepoints' => 'utils/utf8.php',
'phutil_utf8v_combine_characters' => 'utils/utf8.php',
'phutil_utf8v_combined' => 'utils/utf8.php',
'phutil_validate_json' => 'utils/utils.php',
'phutil_var_export' => 'utils/utils.php',
'ppull' => 'utils/utils.php',
'pregsprintf' => 'xsprintf/pregsprintf.php',
'tsprintf' => 'xsprintf/tsprintf.php',
'urisprintf' => 'xsprintf/urisprintf.php',
'vcsprintf' => 'xsprintf/csprintf.php',
'vjsprintf' => 'xsprintf/jsprintf.php',
'vurisprintf' => 'xsprintf/urisprintf.php',
'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
'xsprintf' => 'xsprintf/xsprintf.php',
'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
'xsprintf_command' => 'xsprintf/csprintf.php',
'xsprintf_javascript' => 'xsprintf/jsprintf.php',
'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
'xsprintf_regex' => 'xsprintf/pregsprintf.php',
'xsprintf_terminal' => 'xsprintf/tsprintf.php',
'xsprintf_uri' => 'xsprintf/urisprintf.php',
), ),
'function' => array(),
'xmap' => array( 'xmap' => array(
'AASTNode' => 'Phobject',
'AASTNodeList' => array(
'Phobject',
'Countable',
'Iterator',
),
'AASTToken' => 'Phobject',
'AASTTree' => 'Phobject',
'AbstractDirectedGraph' => 'Phobject',
'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractMethodBodyXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', 'ArcanistAbstractMethodBodyXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase',
'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistAbstractPrivateMethodXHPASTLinterRule' => 'ArcanistXHPASTLinterRule',
@ -843,18 +1293,350 @@ phutil_register_library_map(array(
'ArcanistXMLLinter' => 'ArcanistLinter', 'ArcanistXMLLinter' => 'ArcanistLinter',
'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase', 'ArcanistXMLLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistXUnitTestResultParser' => 'Phobject', 'ArcanistXUnitTestResultParser' => 'Phobject',
'BaseHTTPFuture' => 'Future',
'CSharpToolsTestEngine' => 'XUnitTestEngine', 'CSharpToolsTestEngine' => 'XUnitTestEngine',
'CaseInsensitiveArray' => 'PhutilArray',
'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
'CommandException' => 'Exception',
'ConduitClient' => 'Phobject',
'ConduitClientException' => 'Exception',
'ConduitClientTestCase' => 'PhutilTestCase',
'ConduitFuture' => 'FutureProxy',
'ExecFuture' => 'PhutilExecutableFuture',
'ExecFutureTestCase' => 'PhutilTestCase',
'ExecPassthruTestCase' => 'PhutilTestCase',
'FileFinder' => 'Phobject',
'FileFinderTestCase' => 'PhutilTestCase',
'FileList' => 'Phobject',
'Filesystem' => 'Phobject',
'FilesystemException' => 'Exception',
'FilesystemTestCase' => 'PhutilTestCase',
'Future' => 'Phobject',
'FutureIterator' => array(
'Phobject',
'Iterator',
),
'FutureIteratorTestCase' => 'PhutilTestCase',
'FutureProxy' => 'Future',
'HTTPFuture' => 'BaseHTTPFuture',
'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatus' => 'Exception',
'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPSFuture' => 'BaseHTTPFuture',
'ImmediateFuture' => 'Future',
'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
'LinesOfALarge' => array(
'Phobject',
'Iterator',
),
'LinesOfALargeExecFuture' => 'LinesOfALarge',
'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
'LinesOfALargeFile' => 'LinesOfALarge',
'LinesOfALargeFileTestCase' => 'PhutilTestCase',
'MFilterTestHelper' => 'Phobject',
'NoseTestEngine' => 'ArcanistUnitTestEngine', 'NoseTestEngine' => 'ArcanistUnitTestEngine',
'PHPASTParserTestCase' => 'PhutilTestCase',
'PhageAction' => 'Phobject',
'PhageAgentAction' => 'PhageAction',
'PhageAgentBootloader' => 'Phobject',
'PhageAgentTestCase' => 'PhutilTestCase',
'PhageExecuteAction' => 'PhageAction',
'PhageLocalAction' => 'PhageAgentAction',
'PhagePHPAgent' => 'Phobject',
'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
'PhagePlanAction' => 'PhageAction',
'Phobject' => 'Iterator',
'PhobjectTestCase' => 'PhutilTestCase',
'PhpunitTestEngine' => 'ArcanistUnitTestEngine', 'PhpunitTestEngine' => 'ArcanistUnitTestEngine',
'PhpunitTestEngineTestCase' => 'PhutilTestCase', 'PhpunitTestEngineTestCase' => 'PhutilTestCase',
'PhutilAWSCloudFormationFuture' => 'PhutilAWSFuture',
'PhutilAWSCloudWatchFuture' => 'PhutilAWSFuture',
'PhutilAWSEC2Future' => 'PhutilAWSFuture',
'PhutilAWSException' => 'Exception',
'PhutilAWSFuture' => 'FutureProxy',
'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSS3Future' => 'PhutilAWSFuture',
'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSv4Signature' => 'Phobject',
'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
'PhutilAggregateException' => 'Exception',
'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
'PhutilArgumentParser' => 'Phobject',
'PhutilArgumentParserException' => 'Exception',
'PhutilArgumentParserTestCase' => 'PhutilTestCase',
'PhutilArgumentSpecification' => 'Phobject',
'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
'PhutilArgumentSpellingCorrector' => 'Phobject',
'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase',
'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
'PhutilArgumentWorkflow' => 'Phobject',
'PhutilArray' => array(
'Phobject',
'Countable',
'ArrayAccess',
'Iterator',
),
'PhutilArrayTestCase' => 'PhutilTestCase',
'PhutilArrayWithDefaultValue' => 'PhutilArray',
'PhutilAsanaFuture' => 'FutureProxy',
'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler',
'PhutilBallOfPHP' => 'Phobject',
'PhutilBinaryAnalyzer' => 'Phobject',
'PhutilBinaryAnalyzerTestCase' => 'PhutilTestCase',
'PhutilBootloaderException' => 'Exception',
'PhutilBritishEnglishLocale' => 'PhutilLocale',
'PhutilBufferedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
'PhutilBugtraqParser' => 'Phobject',
'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
'PhutilCIDRBlock' => 'Phobject',
'PhutilCIDRList' => 'Phobject',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
'PhutilChannel' => 'Phobject',
'PhutilChannelChannel' => 'PhutilChannel',
'PhutilChannelTestCase' => 'PhutilTestCase',
'PhutilChunkedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
'PhutilClassMapQuery' => 'Phobject',
'PhutilCloudWatchMetric' => 'Phobject',
'PhutilCommandString' => 'Phobject',
'PhutilConsole' => 'Phobject',
'PhutilConsoleBlock' => 'PhutilConsoleView',
'PhutilConsoleError' => 'PhutilConsoleLogLine',
'PhutilConsoleFormatter' => 'Phobject',
'PhutilConsoleInfo' => 'PhutilConsoleLogLine',
'PhutilConsoleList' => 'PhutilConsoleView',
'PhutilConsoleLogLine' => 'PhutilConsoleView',
'PhutilConsoleMessage' => 'Phobject',
'PhutilConsoleMetrics' => 'Phobject',
'PhutilConsoleMetricsSignalHandler' => 'PhutilSignalHandler',
'PhutilConsoleProgressBar' => 'Phobject',
'PhutilConsoleProgressSink' => 'PhutilProgressSink',
'PhutilConsoleServer' => 'Phobject',
'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
'PhutilConsoleSkip' => 'PhutilConsoleLogLine',
'PhutilConsoleStdinNotInteractiveException' => 'Exception',
'PhutilConsoleTable' => 'PhutilConsoleView',
'PhutilConsoleView' => 'Phobject',
'PhutilConsoleWarning' => 'PhutilConsoleLogLine',
'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
'PhutilCowsay' => 'Phobject',
'PhutilCowsayTestCase' => 'PhutilTestCase',
'PhutilCsprintfTestCase' => 'PhutilTestCase',
'PhutilCzechLocale' => 'PhutilLocale',
'PhutilDOMNode' => 'Phobject',
'PhutilDeferredLog' => 'Phobject',
'PhutilDeferredLogTestCase' => 'PhutilTestCase',
'PhutilDiffBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
'PhutilDirectoryFixture' => 'Phobject',
'PhutilDocblockParser' => 'Phobject',
'PhutilDocblockParserTestCase' => 'PhutilTestCase',
'PhutilEditDistanceMatrix' => 'Phobject',
'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
'PhutilEditorConfig' => 'Phobject',
'PhutilEditorConfigTestCase' => 'PhutilTestCase',
'PhutilEmailAddress' => 'Phobject',
'PhutilEmailAddressTestCase' => 'PhutilTestCase',
'PhutilEmojiLocale' => 'PhutilLocale',
'PhutilEnglishCanadaLocale' => 'PhutilLocale',
'PhutilErrorHandler' => 'Phobject',
'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
'PhutilErrorTrap' => 'Phobject',
'PhutilEvent' => 'Phobject',
'PhutilEventConstants' => 'Phobject',
'PhutilEventEngine' => 'Phobject',
'PhutilEventListener' => 'Phobject',
'PhutilEventType' => 'PhutilEventConstants',
'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
'PhutilExecChannel' => 'PhutilChannel',
'PhutilExecPassthru' => 'PhutilExecutableFuture',
'PhutilExecutableFuture' => 'Future',
'PhutilExecutionEnvironment' => 'Phobject',
'PhutilFileLock' => 'PhutilLock',
'PhutilFileLockTestCase' => 'PhutilTestCase',
'PhutilFileTree' => 'Phobject',
'PhutilFrenchLocale' => 'PhutilLocale',
'PhutilGermanLocale' => 'PhutilLocale',
'PhutilGitBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
'PhutilGitHubFuture' => 'FutureProxy',
'PhutilGitHubResponse' => 'Phobject',
'PhutilGitURI' => 'Phobject',
'PhutilGitURITestCase' => 'PhutilTestCase',
'PhutilHTMLParser' => 'Phobject',
'PhutilHTMLParserTestCase' => 'PhutilTestCase',
'PhutilHTTPEngineExtension' => 'Phobject',
'PhutilHTTPResponse' => 'Phobject',
'PhutilHTTPResponseParser' => 'Phobject',
'PhutilHTTPResponseParserTestCase' => 'PhutilTestCase',
'PhutilHashingIterator' => array(
'PhutilProxyIterator',
'Iterator',
),
'PhutilHashingIteratorTestCase' => 'PhutilTestCase',
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
'PhutilIPAddressTestCase' => 'PhutilTestCase',
'PhutilIPv4Address' => 'PhutilIPAddress',
'PhutilIPv6Address' => 'PhutilIPAddress',
'PhutilInteractiveEditor' => 'Phobject',
'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilInvalidStateException' => 'Exception',
'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilJSON' => 'Phobject',
'PhutilJSONFragmentLexer' => 'PhutilLexer',
'PhutilJSONParser' => 'Phobject',
'PhutilJSONParserException' => 'Exception',
'PhutilJSONParserTestCase' => 'PhutilTestCase',
'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilJSONTestCase' => 'PhutilTestCase',
'PhutilJavaFragmentLexer' => 'PhutilLexer',
'PhutilKoreanLocale' => 'PhutilLocale',
'PhutilLanguageGuesser' => 'Phobject',
'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
'PhutilLexer' => 'Phobject',
'PhutilLibraryConflictException' => 'Exception',
'PhutilLibraryMapBuilder' => 'Phobject',
'PhutilLibraryTestCase' => 'PhutilTestCase',
'PhutilLocale' => 'Phobject',
'PhutilLocaleTestCase' => 'PhutilTestCase',
'PhutilLock' => 'Phobject',
'PhutilLockException' => 'Exception',
'PhutilLogFileChannel' => 'PhutilChannelChannel',
'PhutilLunarPhase' => 'Phobject',
'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
'PhutilMercurialBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
'PhutilMethodNotImplementedException' => 'Exception',
'PhutilMetricsChannel' => 'PhutilChannelChannel',
'PhutilMissingSymbolException' => 'Exception',
'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
'PhutilNumber' => 'Phobject',
'PhutilOAuth1Future' => 'FutureProxy',
'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
'PhutilOpaqueEnvelope' => 'Phobject',
'PhutilOpaqueEnvelopeKey' => 'Phobject',
'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
'PhutilPHPFragmentLexer' => 'PhutilLexer',
'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilParserGenerator' => 'Phobject',
'PhutilParserGeneratorException' => 'Exception',
'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
'PhutilPayPalAPIFuture' => 'FutureProxy',
'PhutilPersonTest' => array(
'Phobject',
'PhutilPerson',
),
'PhutilPhtTestCase' => 'PhutilTestCase',
'PhutilPirateEnglishLocale' => 'PhutilLocale',
'PhutilPortugueseBrazilLocale' => 'PhutilLocale',
'PhutilPortuguesePortugalLocale' => 'PhutilLocale',
'PhutilPostmarkFuture' => 'FutureProxy',
'PhutilPregsprintfTestCase' => 'PhutilTestCase',
'PhutilProcessQuery' => 'Phobject',
'PhutilProcessRef' => 'Phobject',
'PhutilProcessRefTestCase' => 'PhutilTestCase',
'PhutilProgressSink' => 'Phobject',
'PhutilProtocolChannel' => 'PhutilChannelChannel',
'PhutilProxyException' => 'Exception',
'PhutilProxyIterator' => array(
'Phobject',
'Iterator',
),
'PhutilPygmentizeBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
'PhutilPythonFragmentLexer' => 'PhutilLexer',
'PhutilQueryStringParser' => 'Phobject',
'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
'PhutilRawEnglishLocale' => 'PhutilLocale',
'PhutilReadableSerializer' => 'Phobject',
'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
'PhutilRope' => 'Phobject',
'PhutilRopeTestCase' => 'PhutilTestCase',
'PhutilServiceProfiler' => 'Phobject',
'PhutilShellLexer' => 'PhutilLexer',
'PhutilShellLexerTestCase' => 'PhutilTestCase',
'PhutilSignalHandler' => 'Phobject',
'PhutilSignalRouter' => 'Phobject',
'PhutilSimpleOptions' => 'Phobject',
'PhutilSimpleOptionsLexer' => 'PhutilLexer',
'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
'PhutilSimplifiedChineseLocale' => 'PhutilLocale',
'PhutilSlackFuture' => 'FutureProxy',
'PhutilSocketChannel' => 'PhutilChannel',
'PhutilSortVector' => 'Phobject',
'PhutilSpanishSpainLocale' => 'PhutilLocale',
'PhutilStreamIterator' => array(
'Phobject',
'Iterator',
),
'PhutilSubversionBinaryAnalyzer' => 'PhutilBinaryAnalyzer',
'PhutilSystem' => 'Phobject',
'PhutilSystemTestCase' => 'PhutilTestCase',
'PhutilTerminalString' => 'Phobject',
'PhutilTestCase' => 'Phobject', 'PhutilTestCase' => 'Phobject',
'PhutilTestCaseTestCase' => 'PhutilTestCase', 'PhutilTestCaseTestCase' => 'PhutilTestCase',
'PhutilTestPhobject' => 'Phobject',
'PhutilTestSkippedException' => 'Exception', 'PhutilTestSkippedException' => 'Exception',
'PhutilTestTerminatedException' => 'Exception', 'PhutilTestTerminatedException' => 'Exception',
'PhutilTraditionalChineseLocale' => 'PhutilLocale',
'PhutilTranslation' => 'Phobject',
'PhutilTranslationTestCase' => 'PhutilTestCase',
'PhutilTranslator' => 'Phobject',
'PhutilTranslatorTestCase' => 'PhutilTestCase',
'PhutilTsprintfTestCase' => 'PhutilTestCase',
'PhutilTwitchFuture' => 'FutureProxy',
'PhutilTypeCheckException' => 'Exception',
'PhutilTypeExtraParametersException' => 'Exception',
'PhutilTypeLexer' => 'PhutilLexer',
'PhutilTypeMissingParametersException' => 'Exception',
'PhutilTypeSpec' => 'Phobject',
'PhutilTypeSpecTestCase' => 'PhutilTestCase',
'PhutilURI' => 'Phobject',
'PhutilURITestCase' => 'PhutilTestCase',
'PhutilUSEnglishLocale' => 'PhutilLocale',
'PhutilUTF8StringTruncator' => 'Phobject',
'PhutilUTF8TestCase' => 'PhutilTestCase',
'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine', 'PhutilUnitTestEngine' => 'ArcanistUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'PhutilTestCase', 'PhutilUnitTestEngineTestCase' => 'PhutilTestCase',
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUrisprintfTestCase' => 'PhutilTestCase',
'PhutilUtilsTestCase' => 'PhutilTestCase',
'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
'PhutilWordPressFuture' => 'FutureProxy',
'PhutilXHPASTBinary' => 'Phobject',
'PytestTestEngine' => 'ArcanistUnitTestEngine', 'PytestTestEngine' => 'ArcanistUnitTestEngine',
'TempFile' => 'Phobject',
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
'XHPASTNode' => 'AASTNode',
'XHPASTNodeTestCase' => 'PhutilTestCase',
'XHPASTSyntaxErrorException' => 'Exception',
'XHPASTToken' => 'AASTToken',
'XHPASTTree' => 'AASTTree',
'XHPASTTreeTestCase' => 'PhutilTestCase',
'XUnitTestEngine' => 'ArcanistUnitTestEngine', 'XUnitTestEngine' => 'ArcanistUnitTestEngine',
'XUnitTestResultParserTestCase' => 'PhutilTestCase', 'XUnitTestResultParserTestCase' => 'PhutilTestCase',
'XsprintfUnknownConversionException' => 'InvalidArgumentException',
), ),
)); ));

View file

@ -0,0 +1,191 @@
<?php
/**
* @concrete-extensible
*/
class PhutilLibraryTestCase extends PhutilTestCase {
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that all symbols can be loaded correctly. It can catch problems like
* missing methods in descendants of abstract base classes.
*/
public function testEverythingImplemented() {
id(new PhutilSymbolLoader())
->setLibrary($this->getLibraryName())
->selectAndLoadSymbols();
$this->assertTrue(true);
}
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that all the library map is up-to-date.
*/
public function testLibraryMap() {
$root = $this->getLibraryRoot();
$library = phutil_get_library_name_for_root($root);
$new_library_map = id(new PhutilLibraryMapBuilder($root))
->buildMap();
$bootloader = PhutilBootloader::getInstance();
$old_library_map = $bootloader->getLibraryMapWithoutExtensions($library);
unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]);
$identical = ($new_library_map === $old_library_map);
if (!$identical) {
$differences = $this->getMapDifferences(
$old_library_map,
$new_library_map);
sort($differences);
} else {
$differences = array();
}
$this->assertTrue(
$identical,
pht(
"The library map is out of date. Rebuild it with `%s`.\n".
"These entries differ: %s.",
'arc liberate',
implode(', ', $differences)));
}
private function getMapDifferences($old, $new) {
$changed = array();
$all = $old + $new;
foreach ($all as $key => $value) {
$old_exists = array_key_exists($key, $old);
$new_exists = array_key_exists($key, $new);
// One map has it and the other does not, so mark it as changed.
if ($old_exists != $new_exists) {
$changed[] = $key;
continue;
}
$oldv = idx($old, $key);
$newv = idx($new, $key);
if ($oldv === $newv) {
continue;
}
if (is_array($oldv) && is_array($newv)) {
$child_changed = $this->getMapDifferences($oldv, $newv);
foreach ($child_changed as $child) {
$changed[] = $key.'.'.$child;
}
} else {
$changed[] = $key;
}
}
return $changed;
}
/**
* This is more of an acceptance test case instead of a unit test. It verifies
* that methods in subclasses have the same visibility as the method in the
* parent class.
*/
public function testMethodVisibility() {
$symbols = id(new PhutilSymbolLoader())
->setLibrary($this->getLibraryName())
->selectSymbolsWithoutLoading();
$classes = array();
foreach ($symbols as $symbol) {
if ($symbol['type'] == 'class') {
$classes[$symbol['name']] = new ReflectionClass($symbol['name']);
}
}
$failures = array();
foreach ($classes as $class_name => $class) {
$parents = array();
$parent = $class;
while ($parent = $parent->getParentClass()) {
$parents[] = $parent;
}
$interfaces = $class->getInterfaces();
foreach ($class->getMethods() as $method) {
$method_name = $method->getName();
foreach (array_merge($parents, $interfaces) as $extends) {
if ($extends->hasMethod($method_name)) {
$xmethod = $extends->getMethod($method_name);
if (!$this->compareVisibility($xmethod, $method)) {
$failures[] = pht(
'Class "%s" implements method "%s" with the wrong visibility. '.
'The method has visibility "%s", but it is defined in parent '.
'"%s" with visibility "%s". In Phabricator, a method which '.
'overrides another must always have the same visibility.',
$class_name,
$method_name,
$this->getVisibility($method),
$extends->getName(),
$this->getVisibility($xmethod));
}
// We found a declaration somewhere, so stop looking.
break;
}
}
}
}
$this->assertTrue(
empty($failures),
"\n\n".implode("\n\n", $failures));
}
/**
* Get the name of the library currently being tested.
*/
protected function getLibraryName() {
return phutil_get_library_name_for_root($this->getLibraryRoot());
}
/**
* Get the root directory for the library currently being tested.
*/
protected function getLibraryRoot() {
$caller = id(new ReflectionClass($this))->getFileName();
return phutil_get_library_root_for_path($caller);
}
private function compareVisibility(
ReflectionMethod $parent_method,
ReflectionMethod $method) {
static $bitmask;
if ($bitmask === null) {
$bitmask = ReflectionMethod::IS_PUBLIC;
$bitmask += ReflectionMethod::IS_PROTECTED;
$bitmask += ReflectionMethod::IS_PRIVATE;
}
$parent_modifiers = $parent_method->getModifiers();
$modifiers = $method->getModifiers();
return !(($parent_modifiers ^ $modifiers) & $bitmask);
}
private function getVisibility(ReflectionMethod $method) {
if ($method->isPrivate()) {
return 'private';
} else if ($method->isProtected()) {
return 'protected';
} else {
return 'public';
}
}
}

View file

@ -0,0 +1,426 @@
<?php
/**
* Wrapper around streams, pipes, and other things that have basic read/write
* I/O characteristics.
*
* Channels include buffering, so you can do fire-and-forget writes and reads
* without worrying about network/pipe buffers. Use @{method:read} and
* @{method:write} to read and write.
*
* Channels are nonblocking and provide a select()-oriented interface so you
* can reasonably write server-like and daemon-like things with them. Use
* @{method:waitForAny} to select channels.
*
* Channel operations other than @{method:update} generally operate on buffers.
* Writes and reads affect buffers, while @{method:update} flushes output
* buffers and fills input buffers.
*
* Channels are either "open" or "closed". You can detect that a channel has
* closed by calling @{method:isOpen} or examining the return value of
* @{method:update}.
*
* NOTE: Channels are new (as of June 2012) and subject to interface changes.
*
* @task io Reading and Writing
* @task wait Waiting for Activity
* @task update Responding to Activity
* @task impl Channel Implementation
*/
abstract class PhutilChannel extends Phobject {
private $ibuf = '';
private $obuf;
private $name;
private $readBufferSize;
public function __construct() {
$this->obuf = new PhutilRope();
}
/* -( Reading and Writing )------------------------------------------------ */
/**
* Read from the channel. A channel defines the format of data that is read
* from it, so this method may return strings, objects, or anything else.
*
* The default implementation returns bytes.
*
* @return wild Data from the channel, normally bytes.
*
* @task io
*/
public function read() {
$result = $this->ibuf;
$this->ibuf = '';
return $result;
}
/**
* Write to the channel. A channel defines what data format it accepts,
* so this method may take strings, objects, or anything else.
*
* The default implementation accepts bytes.
*
* @param wild Data to write to the channel, normally bytes.
* @return this
*
* @task io
*/
public function write($bytes) {
if (!is_scalar($bytes)) {
throw new Exception(
pht(
'%s may only write strings!',
__METHOD__.'()'));
}
$this->obuf->append($bytes);
return $this;
}
/* -( Waiting for Activity )----------------------------------------------- */
/**
* Wait for any activity on a list of channels. Convenience wrapper around
* @{method:waitForActivity}.
*
* @param list<PhutilChannel> A list of channels to wait for.
* @param dict Options, see above.
* @return void
*
* @task wait
*/
public static function waitForAny(array $channels, array $options = array()) {
return self::waitForActivity($channels, $channels, $options);
}
/**
* Wait (using select()) for channels to become ready for reads or writes.
* This method blocks until some channel is ready to be updated.
*
* It does not provide a way to determine which channels are ready to be
* updated. The expectation is that you'll just update every channel. This
* might change eventually.
*
* Available options are:
*
* - 'read' (list<stream>) Additional streams to select for read.
* - 'write' (list<stream>) Additional streams to select for write.
* - 'except' (list<stream>) Additional streams to select for except.
* - 'timeout' (float) Select timeout, defaults to 1.
*
* NOTE: Extra streams must be //streams//, not //sockets//, because this
* method uses `stream_select()`, not `socket_select()`.
*
* @param list<PhutilChannel> List of channels to wait for reads on.
* @param list<PhutilChannel> List of channels to wait for writes on.
* @return void
*
* @task wait
*/
public static function waitForActivity(
array $reads,
array $writes,
array $options = array()) {
assert_instances_of($reads, __CLASS__);
assert_instances_of($writes, __CLASS__);
$read = idx($options, 'read', array());
$write = idx($options, 'write', array());
$except = idx($options, 'except', array());
$wait = idx($options, 'timeout', 1);
// TODO: It would be nice to just be able to categorically reject these as
// unselectable.
foreach (array($reads, $writes) as $channels) {
foreach ($channels as $channel) {
$r_sockets = $channel->getReadSockets();
$w_sockets = $channel->getWriteSockets();
// If any channel has no read sockets and no write sockets, assume it
// isn't selectable and return immediately (effectively degrading to a
// busy wait).
if (!$r_sockets && !$w_sockets) {
return false;
}
}
}
foreach ($reads as $channel) {
// If any of the read channels have data in read buffers, return
// immediately. If we don't, we risk running select() on a bunch of
// sockets which won't become readable because the data the application
// expects is already in a read buffer.
if (!$channel->isReadBufferEmpty()) {
return;
}
$r_sockets = $channel->getReadSockets();
foreach ($r_sockets as $socket) {
$read[] = $socket;
$except[] = $socket;
}
}
foreach ($writes as $channel) {
if ($channel->isWriteBufferEmpty()) {
// If the channel's write buffer is empty, don't select the write
// sockets, since they're writable immediately.
$w_sockets = array();
} else {
$w_sockets = $channel->getWriteSockets();
}
foreach ($w_sockets as $socket) {
$write[] = $socket;
$except[] = $socket;
}
}
if (!$read && !$write && !$except) {
return false;
}
$wait_sec = (int)$wait;
$wait_usec = 1000000 * ($wait - $wait_sec);
@stream_select($read, $write, $except, $wait_sec, $wait_usec);
}
/* -( Responding to Activity )--------------------------------------------- */
/**
* Updates the channel, filling input buffers and flushing output buffers.
* Returns false if the channel has closed.
*
* @return bool True if the channel is still open.
*
* @task update
*/
public function update() {
$maximum_read = PHP_INT_MAX;
if ($this->readBufferSize !== null) {
$maximum_read = ($this->readBufferSize - strlen($this->ibuf));
}
while ($maximum_read > 0) {
$in = $this->readBytes($maximum_read);
if (!strlen($in)) {
// Reading is blocked for now.
break;
}
$this->ibuf .= $in;
$maximum_read -= strlen($in);
}
while ($this->obuf->getByteLength()) {
$len = $this->writeBytes($this->obuf->getAnyPrefix());
if (!$len) {
// Writing is blocked for now.
break;
}
$this->obuf->removeBytesFromHead($len);
}
return $this->isOpen();
}
/* -( Channel Implementation )--------------------------------------------- */
/**
* Set a channel name. This is primarily intended to allow you to debug
* channel code more easily, by naming channels something meaningful.
*
* @param string Channel name.
* @return this
*
* @task impl
*/
public function setName($name) {
$this->name = $name;
return $this;
}
/**
* Get the channel name, as set by @{method:setName}.
*
* @return string Name of the channel.
*
* @task impl
*/
public function getName() {
return coalesce($this->name, get_class($this));
}
/**
* Test if the channel is open: active, can be read from and written to, etc.
*
* @return bool True if the channel is open.
*
* @task impl
*/
abstract public function isOpen();
/**
* Close the channel for writing.
*
* @return void
* @task impl
*/
abstract public function closeWriteChannel();
/**
* Test if the channel is open for reading.
*
* @return bool True if the channel is open for reading.
*
* @task impl
*/
public function isOpenForReading() {
return $this->isOpen();
}
/**
* Test if the channel is open for writing.
*
* @return bool True if the channel is open for writing.
*
* @task impl
*/
public function isOpenForWriting() {
return $this->isOpen();
}
/**
* Read from the channel's underlying I/O.
*
* @param int Maximum number of bytes to read.
* @return string Bytes, if available.
*
* @task impl
*/
abstract protected function readBytes($length);
/**
* Write to the channel's underlying I/O.
*
* @param string Bytes to write.
* @return int Number of bytes written.
*
* @task impl
*/
abstract protected function writeBytes($bytes);
/**
* Get sockets to select for reading.
*
* @return list<stream> Read sockets.
*
* @task impl
*/
protected function getReadSockets() {
return array();
}
/**
* Get sockets to select for writing.
*
* @return list<stream> Write sockets.
*
* @task impl
*/
protected function getWriteSockets() {
return array();
}
/**
* Set the maximum size of the channel's read buffer. Reads will artificially
* block once the buffer reaches this size until the in-process buffer is
* consumed.
*
* @param int|null Maximum read buffer size, or `null` for a limitless buffer.
* @return this
* @task impl
*/
public function setReadBufferSize($size) {
$this->readBufferSize = $size;
return $this;
}
/**
* Test state of the read buffer.
*
* @return bool True if the read buffer is empty.
*
* @task impl
*/
public function isReadBufferEmpty() {
return (strlen($this->ibuf) == 0);
}
/**
* Test state of the write buffer.
*
* @return bool True if the write buffer is empty.
*
* @task impl
*/
public function isWriteBufferEmpty() {
return !$this->getWriteBufferSize();
}
/**
* Get the number of bytes we're currently waiting to write.
*
* @return int Number of waiting bytes.
*
* @task impl
*/
public function getWriteBufferSize() {
return $this->obuf->getByteLength();
}
/**
* Wait for any buffered writes to complete. This is a blocking call. When
* the call returns, the write buffer will be empty.
*
* @task impl
*/
public function flush() {
while (!$this->isWriteBufferEmpty()) {
self::waitForAny(array($this));
if (!$this->update()) {
throw new Exception(pht('Channel closed while flushing output!'));
}
}
return $this;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Channel that wraps some other channel. This class is not interesting on its
* own and just serves as a no-op proxy, but extending it allows you to compose
* channels to mutate their characteristics (for instance, to add protocol
* semantics with @{class:PhutilProtocolChannel}).
*
* The implementation of this class is entirely uninteresting.
*/
abstract class PhutilChannelChannel extends PhutilChannel {
private $channel;
public function __construct(PhutilChannel $channel) {
parent::__construct();
$this->channel = $channel;
$this->didConstruct();
}
protected function didConstruct() {
// Hook for subclasses.
}
public function read() {
return $this->channel->read();
}
public function write($message) {
$this->channel->write($message);
return $this;
}
public function update() {
return $this->channel->update();
}
public function isOpen() {
return $this->channel->isOpen();
}
public function closeWriteChannel() {
return $this->channel->closeWriteChannel();
}
public function isOpenForReading() {
return $this->channel->isOpenForReading();
}
public function isOpenForWriting() {
return $this->channel->isOpenForWriting();
}
protected function readBytes($length) {
$this->throwOnRawByteOperations();
}
protected function writeBytes($bytes) {
$this->throwOnRawByteOperations();
}
protected function getReadSockets() {
return $this->channel->getReadSockets();
}
protected function getWriteSockets() {
return $this->channel->getWriteSockets();
}
public function setReadBufferSize($size) {
$this->channel->setReadBufferSize($size);
return $this;
}
public function isReadBufferEmpty() {
return $this->channel->isReadBufferEmpty();
}
public function isWriteBufferEmpty() {
return $this->channel->isWriteBufferEmpty();
}
public function getWriteBufferSize() {
return $this->channel->getWriteBufferSize();
}
public function flush() {
$this->channel->flush();
return $this;
}
protected function getUnderlyingChannel() {
return $this->channel;
}
private function throwOnRawByteOperations() {
// NOTE: You should only be able to end up here if you subclass this class
// and implement your subclass incorrectly, since the byte methods are
// protected.
throw new Exception(
pht(
'Do not call %s or %s directly on a %s. Instead, call %s or %s.',
'readBytes()',
'writeBytes()',
__CLASS__,
'read()',
'write()'));
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Channel on an underlying @{class:ExecFuture}. For a description of channels,
* see @{class:PhutilChannel}.
*
* For example, you can open a channel on `nc` like this:
*
* $future = new ExecFuture('nc example.com 80');
* $channel = new PhutilExecChannel($future);
*
* $channel->write("GET / HTTP/1.0\n\n");
* while (true) {
* echo $channel->read();
*
* PhutilChannel::waitForAny(array($channel));
* if (!$channel->update()) {
* // Break out of the loop when the channel closes.
* break;
* }
* }
*
* This script makes an HTTP request to "example.com". This example is heavily
* contrived. In most cases, @{class:ExecFuture} and other futures constructs
* offer a much easier way to solve problems which involve system commands, and
* @{class:HTTPFuture} and other HTTP constructs offer a much easier way to
* solve problems which involve HTTP.
*
* @{class:PhutilExecChannel} is generally useful only when a program acts like
* a server but performs I/O on stdin/stdout, and you need to act like a client
* or interact with the program at the same time as you manage traditional
* socket connections. Examples are Mercurial operating in "cmdserve" mode, git
* operating in "receive-pack" mode, etc. It is unlikely that any reasonable
* use of this class is concise enough to make a short example out of, so you
* get a contrived one instead.
*
* See also @{class:PhutilSocketChannel}, for a similar channel that uses
* sockets for I/O.
*
* Since @{class:ExecFuture} already supports buffered I/O and socket selection,
* the implementation of this class is fairly straightforward.
*
* @task construct Construction
*/
final class PhutilExecChannel extends PhutilChannel {
private $future;
private $stderrHandler;
/* -( Construction )------------------------------------------------------- */
/**
* Construct an exec channel from a @{class:ExecFuture}. The future should
* **NOT** have been started yet (e.g., with `isReady()` or `start()`),
* because @{class:ExecFuture} closes stdin by default when futures start.
* If stdin has been closed, you will be unable to write on the channel.
*
* @param ExecFuture Future to use as an underlying I/O source.
* @task construct
*/
public function __construct(ExecFuture $future) {
parent::__construct();
// Make an empty write to keep the stdin pipe open. By default, futures
// close this pipe when they start.
$future->write('', $keep_pipe = true);
// Start the future so that reads and writes work immediately.
$future->isReady();
$this->future = $future;
}
public function __destruct() {
if (!$this->future->isReady()) {
$this->future->resolveKill();
}
}
public function update() {
$this->future->isReady();
return parent::update();
}
public function isOpen() {
return !$this->future->isReady();
}
protected function readBytes($length) {
list($stdout, $stderr) = $this->future->read();
$this->future->discardBuffers();
if (strlen($stderr)) {
if ($this->stderrHandler) {
call_user_func($this->stderrHandler, $this, $stderr);
} else {
throw new Exception(
pht('Unexpected output to stderr on exec channel: %s', $stderr));
}
}
return $stdout;
}
public function write($bytes) {
$this->future->write($bytes, $keep_pipe = true);
}
public function closeWriteChannel() {
$this->future->write('', $keep_pipe = false);
}
protected function writeBytes($bytes) {
throw new Exception(pht('%s can not write bytes directly!', 'ExecFuture'));
}
protected function getReadSockets() {
return $this->future->getReadSockets();
}
protected function getWriteSockets() {
return $this->future->getWriteSockets();
}
public function isReadBufferEmpty() {
// Check both the channel and future read buffers, since either could have
// data.
return parent::isReadBufferEmpty() && $this->future->isReadBufferEmpty();
}
public function setReadBufferSize($size) {
// NOTE: We may end up using 2x the buffer size here, one inside
// ExecFuture and one inside the Channel. We could tune this eventually, but
// it should be fine for now.
parent::setReadBufferSize($size);
$this->future->setReadBufferSize($size);
return $this;
}
public function isWriteBufferEmpty() {
return $this->future->isWriteBufferEmpty();
}
public function getWriteBufferSize() {
return $this->future->getWriteBufferSize();
}
/**
* If the wrapped @{class:ExecFuture} outputs data to stderr, we normally
* throw an exception. Instead, you can provide a callback handler that will
* be invoked and passed the data. It should have this signature:
*
* function f(PhutilExecChannel $channel, $stderr) {
* // ...
* }
*
* The `$channel` will be this channel object, and `$stderr` will be a string
* with bytes received over stderr.
*
* You can set a handler which does nothing to effectively ignore and discard
* any output on stderr.
*
* @param callable Handler to invoke when stderr data is received.
* @return this
*/
public function setStderrHandler($handler) {
$this->stderrHandler = $handler;
return $this;
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Channel that transmits dictionaries of primitives using JSON serialization.
* This channel is not binary safe.
*
* This protocol is implemented by the Phabricator Aphlict realtime notification
* server.
*
* @task protocol Protocol Implementation
*/
final class PhutilJSONProtocolChannel extends PhutilProtocolChannel {
const MODE_LENGTH = 'length';
const MODE_OBJECT = 'object';
/**
* Size of the "length" frame of the protocol in bytes.
*/
const SIZE_LENGTH = 8;
private $mode = self::MODE_LENGTH;
private $byteLengthOfNextChunk = self::SIZE_LENGTH;
private $buf = '';
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission over the channel. The message should
* be any serializable as JSON.
*
* Objects are transmitted as:
*
* <len><json>
*
* ...where <len> is an 8-character, zero-padded integer written as a string.
* For example, this is a valid message:
*
* 00000015{"key":"value"}
*
* @task protocol
*/
protected function encodeMessage($message) {
$message = json_encode($message);
$len = sprintf(
'%0'.self::SIZE_LENGTH.'.'.self::SIZE_LENGTH.'d',
strlen($message));
return "{$len}{$message}";
}
/**
* Decode a message received from the other end of the channel. Messages are
* decoded as associative arrays.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
$objects = array();
while (strlen($this->buf) >= $this->byteLengthOfNextChunk) {
switch ($this->mode) {
case self::MODE_LENGTH:
$len = substr($this->buf, 0, self::SIZE_LENGTH);
$this->buf = substr($this->buf, self::SIZE_LENGTH);
$this->mode = self::MODE_OBJECT;
$this->byteLengthOfNextChunk = (int)$len;
break;
case self::MODE_OBJECT:
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
try {
$objects[] = phutil_json_decode($data);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Failed to decode JSON object.'),
$ex);
}
$this->mode = self::MODE_LENGTH;
$this->byteLengthOfNextChunk = self::SIZE_LENGTH;
break;
}
}
return $objects;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* A @{class:PhutilChannelChannel} which wraps some other channel and writes
* data passed over it to a log file.
*/
final class PhutilLogFileChannel extends PhutilChannelChannel {
private $logfile;
public function setLogfile($path) {
$this->logfile = fopen($path, 'a');
$this->log('--- '.getmypid().' ---');
return $this;
}
public function read() {
$buffer = parent::read();
if (strlen($buffer)) {
$this->log('>>> '.phutil_loggable_string($buffer));
}
return $buffer;
}
public function write($message) {
if (strlen($message)) {
$this->log('<<< '.phutil_loggable_string($message));
}
return parent::write($message);
}
private function log($message) {
if ($this->logfile) {
fwrite($this->logfile, $message."\n");
}
}
}

View file

@ -0,0 +1,85 @@
<?php
/**
* A @{class:PhutilChannelChannel} which wraps some other channel and provides
* metrics about its use (e.g., bytes read and bytes written).
*
* @task metrics Channel Metrics
* @task impl Implementation
*/
final class PhutilMetricsChannel extends PhutilChannelChannel {
private $bytesRead = 0;
private $bytesWritten = 0;
private $startTime;
/* -( Channel Metrics )---------------------------------------------------- */
/**
* Get the number of bytes that have been written to the channel. This
* includes any bytes which have been buffered but not actually transmitted,
* and thus may overreport compared to actual activity on the wire.
*
* @return int Bytes written.
* @task metrics
*/
public function getBytesWritten() {
return $this->bytesWritten;
}
/**
* Get the number of bytes that have been read from the channel. This excludes
* any bytes which have been received but not actually read by anything, and
* thus may underreport compared to actual activity on the wire.
*
* @return int Bytes read.
* @task metrics
*/
public function getBytesRead() {
return $this->bytesRead;
}
/**
* Get the elapsed wall time since this channel opened.
*
* @return float Wall time, in seconds.
* @task metrics
*/
public function getWallTime() {
return microtime(true) - $this->startTime;
}
/* -( Implementation )----------------------------------------------------- */
/**
* @task impl
*/
protected function didConstruct() {
$this->startTime = microtime(true);
}
/**
* @task impl
*/
public function read() {
$buffer = parent::read();
$this->bytesRead += strlen($buffer);
return $buffer;
}
/**
* @task impl
*/
public function write($message) {
$this->bytesWritten += strlen($message);
return parent::write($message);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* Channel that transmits PHP objects using PHP serialization. This channel
* is binary safe.
*
* @task protocol Protocol Implementation
*/
final class PhutilPHPObjectProtocolChannel extends PhutilProtocolChannel {
const MODE_LENGTH = 'length';
const MODE_OBJECT = 'object';
/**
* Size of the "length" frame of the protocol in bytes.
*/
const SIZE_LENGTH = 4;
private $mode = self::MODE_LENGTH;
private $byteLengthOfNextChunk = self::SIZE_LENGTH;
private $buf = '';
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission over the channel. The message should
* be any serializable PHP object. The entire object will be serialized, so
* avoid transmitting objects which connect to large graphs of other objects,
* etc.
*
* This channel can transmit class instances, but the receiving end must be
* running the same version of the code. There are no builtin safeguards
* to protect against versioning problems in object serialization.
*
* Objects are transmitted as:
*
* <len><serialized PHP object>
*
* ...where <len> is a 4-byte unsigned big-endian integer.
*
* @task protocol
*/
protected function encodeMessage($message) {
$message = serialize($message);
$len = pack('N', strlen($message));
return "{$len}{$message}";
}
/**
* Decode a message received from the other end of the channel.
*
* @task protocol
*/
protected function decodeStream($data) {
$this->buf .= $data;
$objects = array();
while (strlen($this->buf) >= $this->byteLengthOfNextChunk) {
switch ($this->mode) {
case self::MODE_LENGTH:
$len = substr($this->buf, 0, self::SIZE_LENGTH);
$this->buf = substr($this->buf, self::SIZE_LENGTH);
$this->mode = self::MODE_OBJECT;
$this->byteLengthOfNextChunk = head(unpack('N', $len));
break;
case self::MODE_OBJECT:
$data = substr($this->buf, 0, $this->byteLengthOfNextChunk);
$this->buf = substr($this->buf, $this->byteLengthOfNextChunk);
$obj = @unserialize($data);
if ($obj === false) {
throw new Exception(pht('Failed to unserialize object: %s', $data));
} else {
$objects[] = $obj;
}
$this->mode = self::MODE_LENGTH;
$this->byteLengthOfNextChunk = self::SIZE_LENGTH;
break;
}
}
return $objects;
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* Wraps a @{class:PhutilChannel} and implements a message-oriented protocol
* on top of it. A protocol channel behaves like a normal channel, except that
* @{method:read} and @{method:write} are message-oriented and the protocol
* channel handles encoding and parsing messages for transmission.
*
* @task io Reading and Writing
* @task protocol Protocol Implementation
* @task wait Waiting for Activity
*/
abstract class PhutilProtocolChannel extends PhutilChannelChannel {
private $messages = array();
/* -( Reading and Writing )------------------------------------------------ */
/**
* Read a message from the channel, if a message is available.
*
* @return wild A message, or null if no message is available.
*
* @task io
*/
public function read() {
$data = parent::read();
if (strlen($data)) {
$messages = $this->decodeStream($data);
foreach ($messages as $message) {
$this->addMessage($message);
}
}
if (!$this->messages) {
return null;
}
return array_shift($this->messages);
}
/**
* Write a message to the channel.
*
* @param wild Some message.
* @return this
*
* @task io
*/
public function write($message) {
$bytes = $this->encodeMessage($message);
return parent::write($bytes);
}
/**
* Add a message to the queue. While you normally do not need to do this,
* you can use it to inject out-of-band messages.
*
* @param wild Some message.
* @return this
*
* @task io
*/
public function addMessage($message) {
$this->messages[] = $message;
return $this;
}
/* -( Protocol Implementation )-------------------------------------------- */
/**
* Encode a message for transmission.
*
* @param wild Some message.
* @return string The message serialized into a wire format for
* transmission.
*
* @task protocol
*/
abstract protected function encodeMessage($message);
/**
* Decode bytes from the underlying channel into zero or more complete
* messages. The messages should be returned.
*
* This method is called as data is available. It will receive incoming
* data only once, and must buffer any data which represents only part of
* a message. Once a complete message is received, it can return the message
* and discard that part of the buffer.
*
* Generally, a protocol channel should maintain a read buffer, implement
* a parser in this method, and store parser state on the object to be able
* to process incoming data in small chunks.
*
* @param string One or more bytes from the underlying channel.
* @return list<wild> Zero or more parsed messages.
*
* @task protocol
*/
abstract protected function decodeStream($data);
/* -( Waiting for Activity )----------------------------------------------- */
/**
* Wait for a message, blocking until one is available.
*
* @return wild A message.
*
* @task wait
*/
public function waitForMessage() {
while (true) {
$is_open = $this->update();
$message = $this->read();
if ($message !== null) {
return $message;
}
if (!$is_open) {
break;
}
self::waitForAny(array($this));
}
throw new Exception(pht('Channel closed while waiting for message!'));
}
}

View file

@ -0,0 +1,192 @@
<?php
/**
* Channel on an underlying stream socket or socket pair. For a description of
* channels, see @{class:PhutilChannel}.
*
* Using a network socket:
*
* $socket = stream_socket_client(...);
* $channel = new PhutilSocketChannel($socket);
*
* Using stdin/stdout:
*
* $channel = new PhutilSocketChannel(
* fopen('php://stdin', 'r'),
* fopen('php://stdout', 'w'));
*
* @task construct Construction
*/
final class PhutilSocketChannel extends PhutilChannel {
private $readSocket;
private $writeSocket;
private $isSingleSocket;
/* -( Construction )------------------------------------------------------- */
/**
* Construct a socket channel from a socket or a socket pair.
*
* NOTE: This must be a //stream// socket from `stream_socket_client()` or
* `stream_socket_server()` or similar, not a //plain// socket from
* `socket_create()` or similar.
*
* @param socket Socket (stream socket, not plain socket). If only one
* socket is provided, it is used for reading and writing.
* @param socket? Optional write socket.
*
* @task construct
*/
public function __construct($read_socket, $write_socket = null) {
parent::__construct();
foreach (array($read_socket, $write_socket) as $socket) {
if (!$socket) {
continue;
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception(pht('Failed to set socket nonblocking!'));
}
}
$this->readSocket = $read_socket;
if ($write_socket) {
$this->writeSocket = $write_socket;
} else {
$this->writeSocket = $read_socket;
$this->isSingleSocket = true;
}
}
public function __destruct() {
$this->closeSockets();
}
/**
* Creates a pair of socket channels that are connected to each other. This
* is mostly useful for writing unit tests of, e.g., protocol channels.
*
* list($x, $y) = PhutilSocketChannel::newChannelPair();
*
* @task construct
*/
public static function newChannelPair() {
$sockets = null;
$domain = phutil_is_windows() ? STREAM_PF_INET : STREAM_PF_UNIX;
$pair = stream_socket_pair($domain, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
if (!$pair) {
throw new Exception(pht('%s failed!', 'stream_socket_pair()'));
}
$x = new PhutilSocketChannel($pair[0]);
$y = new PhutilSocketChannel($pair[1]);
return array($x, $y);
}
public function isOpen() {
return ($this->isOpenForReading() || $this->isOpenForWriting());
}
public function isOpenForReading() {
return (bool)$this->readSocket;
}
public function isOpenForWriting() {
return (bool)$this->writeSocket;
}
protected function readBytes($length) {
$socket = $this->readSocket;
if (!$socket) {
return '';
}
$data = @fread($socket, min($length, 64 * 1024));
if ($data === false) {
$this->closeReadSocket();
$data = '';
}
// NOTE: fread() continues returning empty string after the socket is
// closed, we need to check for EOF explicitly.
if ($data === '') {
if (feof($socket)) {
$this->closeReadSocket();
}
}
return $data;
}
protected function writeBytes($bytes) {
$socket = $this->writeSocket;
if (!$socket) {
return 0;
}
$len = phutil_fwrite_nonblocking_stream($socket, $bytes);
if ($len === false) {
$this->closeWriteSocket();
return 0;
}
return $len;
}
protected function getReadSockets() {
if ($this->readSocket) {
return array($this->readSocket);
}
return array();
}
protected function getWriteSockets() {
if ($this->writeSocket) {
return array($this->writeSocket);
} else {
return array();
}
}
private function closeReadSocket() {
$this->closeOneSocket($this->readSocket);
$this->readSocket = null;
if ($this->isSingleSocket) {
$this->writeSocket = null;
}
}
private function closeWriteSocket() {
$this->closeOneSocket($this->writeSocket);
$this->writeSocket = null;
if ($this->isSingleSocket) {
$this->readSocket = null;
}
}
public function closeWriteChannel() {
$this->closeWriteSocket();
}
private function closeOneSocket($socket) {
if (!$socket) {
return;
}
// We should also stream_socket_shutdown() here but HHVM throws errors
// with it (for example 'Unexpected object type PlainFile'). We depend
// just on fclose() until it is fixed.
@fclose($socket);
}
private function closeSockets() {
$this->closeReadSocket();
$this->closeWriteSocket();
}
}

View file

@ -0,0 +1,45 @@
<?php
final class PhutilChannelTestCase extends PhutilTestCase {
public function testChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$str_len_8 = 'abcdefgh';
$str_len_4 = 'abcd';
// Do a write with no buffer limit.
$x->write($str_len_8);
while (true) {
$x->update();
$y->update();
$read = $y->read();
if (strlen($read)) {
break;
}
}
// We expect to read the entire message.
$this->assertEqual($str_len_8, $read);
// Again, with a read buffer limit.
$y->setReadBufferSize(4);
$x->write($str_len_8);
while (true) {
$x->update();
$y->update();
$read = $y->read();
if (strlen($read)) {
break;
}
}
// We expect to see only the first 4 bytes of the message.
$this->assertEqual($str_len_4, $read);
}
}

View file

@ -0,0 +1,26 @@
<?php
final class PhutilJSONProtocolChannelTestCase extends PhutilTestCase {
public function testJSONChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilJSONProtocolChannel($x);
$yp = new PhutilJSONProtocolChannel($y);
$dict = array(
'rand' => mt_rand(),
'list' => array(1, 2, 3),
'null' => null,
);
$xp->write($dict);
$xp->flush();
$result = $yp->waitForMessage();
$this->assertEqual(
$dict,
$result,
pht('Values are identical.'));
}
}

View file

@ -0,0 +1,66 @@
<?php
final class PhutilPHPObjectProtocolChannelTestCase extends PhutilTestCase {
public function testPHPObjectChannelBasics() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilPHPObjectProtocolChannel($x);
$yp = new PhutilPHPObjectProtocolChannel($y);
$object = (object)array(
'key' => mt_rand(),
);
$xp->write($object);
$xp->flush();
$result = $yp->waitForMessage();
$this->assertTrue(
(array)$object === (array)$result,
pht('Values are identical.'));
$this->assertFalse(
$object === $result,
pht('Objects are not the same.'));
}
public function testCloseSocketWriteChannel() {
list($x, $y) = PhutilSocketChannel::newChannelPair();
$xp = new PhutilPHPObjectProtocolChannel($x);
$yp = new PhutilPHPObjectProtocolChannel($y);
$yp->closeWriteChannel();
$yp->update();
// NOTE: This test is more broad than the implementation needs to be. A
// better test would be to verify that this throws an exception:
//
// $xp->waitForMessage();
//
// However, if the test breaks, that method will hang forever instead of
// returning, which would be hard to diagnose. Since the current
// implementation shuts down the entire channel, just test for that.
$this->assertFalse($xp->update(), pht('Expected channel to close.'));
}
public function testCloseExecWriteChannel() {
$future = new ExecFuture('cat');
// If this test breaks, we want to explode, not hang forever.
$future->setTimeout(5);
$exec_channel = new PhutilExecChannel($future);
$exec_channel->write('quack');
$exec_channel->closeWriteChannel();
// If `closeWriteChannel()` did what it is supposed to, this will just
// echo "quack" and exit with no error code. If the channel did not close,
// this will time out after 5 seconds and throw.
$future->resolvex();
$this->assertTrue(true);
}
}

View file

@ -0,0 +1,395 @@
<?php
final class ConduitClient extends Phobject {
private $uri;
private $host;
private $connectionID;
private $sessionKey;
private $timeout = 300.0;
private $username;
private $password;
private $publicKey;
private $privateKey;
private $conduitToken;
private $oauthToken;
const AUTH_ASYMMETRIC = 'asymmetric';
const SIGNATURE_CONSIGN_1 = 'Consign1.0/';
public function getConnectionID() {
return $this->connectionID;
}
public function __construct($uri) {
$this->uri = new PhutilURI($uri);
if (!strlen($this->uri->getDomain())) {
throw new Exception(
pht("Conduit URI '%s' must include a valid host.", $uri));
}
$this->host = $this->uri->getDomain();
}
/**
* Override the domain specified in the service URI and provide a specific
* host identity.
*
* This can be used to connect to a specific node in a cluster environment.
*/
public function setHost($host) {
$this->host = $host;
return $this;
}
public function getHost() {
return $this->host;
}
public function setConduitToken($conduit_token) {
$this->conduitToken = $conduit_token;
return $this;
}
public function getConduitToken() {
return $this->conduitToken;
}
public function setOAuthToken($oauth_token) {
$this->oauthToken = $oauth_token;
return $this;
}
public function callMethodSynchronous($method, array $params) {
return $this->callMethod($method, $params)->resolve();
}
public function didReceiveResponse($method, $data) {
if ($method == 'conduit.connect') {
$this->sessionKey = idx($data, 'sessionKey');
$this->connectionID = idx($data, 'connectionID');
}
return $data;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function setSigningKeys(
$public_key,
PhutilOpaqueEnvelope $private_key) {
$this->publicKey = $public_key;
$this->privateKey = $private_key;
return $this;
}
public function callMethod($method, array $params) {
$meta = array();
if ($this->sessionKey) {
$meta['sessionKey'] = $this->sessionKey;
}
if ($this->connectionID) {
$meta['connectionID'] = $this->connectionID;
}
if ($method == 'conduit.connect') {
$certificate = idx($params, 'certificate');
if ($certificate) {
$token = time();
$params['authToken'] = $token;
$params['authSignature'] = sha1($token.$certificate);
}
unset($params['certificate']);
}
if ($this->privateKey && $this->publicKey) {
$meta['auth.type'] = self::AUTH_ASYMMETRIC;
$meta['auth.key'] = $this->publicKey;
$meta['auth.host'] = $this->getHostStringForSignature();
$signature = $this->signRequest($method, $params, $meta);
$meta['auth.signature'] = $signature;
}
if ($this->conduitToken) {
$meta['token'] = $this->conduitToken;
}
if ($this->oauthToken) {
$meta['access_token'] = $this->oauthToken;
}
if ($meta) {
$params['__conduit__'] = $meta;
}
$uri = id(clone $this->uri)->setPath('/api/'.$method);
$data = array(
'params' => json_encode($params),
'output' => 'json',
// This is a hint to Phabricator that the client expects a Conduit
// response. It is not necessary, but provides better error messages in
// some cases.
'__conduit__' => true,
);
// Always use the cURL-based HTTPSFuture, for proxy support and other
// protocol edge cases that HTTPFuture does not support.
$core_future = new HTTPSFuture($uri, $data);
$core_future->addHeader('Host', $this->getHostStringForHeader());
$core_future->setMethod('POST');
$core_future->setTimeout($this->timeout);
if ($this->username !== null) {
$core_future->setHTTPBasicAuthCredentials(
$this->username,
$this->password);
}
return id(new ConduitFuture($core_future))
->setClient($this, $method);
}
public function setBasicAuthCredentials($username, $password) {
$this->username = $username;
$this->password = new PhutilOpaqueEnvelope($password);
return $this;
}
private function getHostStringForHeader() {
return $this->newHostString(false);
}
private function getHostStringForSignature() {
return $this->newHostString(true);
}
/**
* Build a string describing the host for this request.
*
* This method builds strings in two modes: with explicit ports for request
* signing (which always include the port number) and with implicit ports
* for use in the "Host:" header of requests (which omit the port number if
* the port is the same as the default port for the protocol).
*
* This implicit port behavior is similar to what browsers do, so it is less
* likely to get us into trouble with webserver configurations.
*
* @param bool True to include the port explicitly.
* @return string String describing the host for the request.
*/
private function newHostString($with_explicit_port) {
$host = $this->getHost();
$uri = new PhutilURI($this->uri);
$protocol = $uri->getProtocol();
$port = $uri->getPort();
$implicit_ports = array(
'https' => 443,
);
$default_port = 80;
$implicit_port = idx($implicit_ports, $protocol, $default_port);
if ($with_explicit_port) {
if (!$port) {
$port = $implicit_port;
}
} else {
if ($port == $implicit_port) {
$port = null;
}
}
if (!$port) {
$result = $host;
} else {
$result = $host.':'.$port;
}
return $result;
}
private function signRequest(
$method,
array $params,
array $meta) {
$input = self::encodeRequestDataForSignature(
$method,
$params,
$meta);
$signature = null;
$result = openssl_sign(
$input,
$signature,
$this->privateKey->openEnvelope());
if (!$result) {
throw new Exception(
pht('Unable to sign Conduit request with signing key.'));
}
return self::SIGNATURE_CONSIGN_1.base64_encode($signature);
}
public static function verifySignature(
$method,
array $params,
array $meta,
$openssl_public_key) {
$auth_type = idx($meta, 'auth.type');
switch ($auth_type) {
case self::AUTH_ASYMMETRIC:
break;
default:
throw new Exception(
pht(
'Unable to verify request signature, specified "%s" '.
'("%s") is unknown.',
'auth.type',
$auth_type));
}
$public_key = idx($meta, 'auth.key');
if (!strlen($public_key)) {
throw new Exception(
pht(
'Unable to verify request signature, no "%s" present in '.
'request protocol information.',
'auth.key'));
}
$signature = idx($meta, 'auth.signature');
if (!strlen($signature)) {
throw new Exception(
pht(
'Unable to verify request signature, no "%s" present '.
'in request protocol information.',
'auth.signature'));
}
$prefix = self::SIGNATURE_CONSIGN_1;
if (strncmp($signature, $prefix, strlen($prefix)) !== 0) {
throw new Exception(
pht(
'Unable to verify request signature, signature format is not '.
'known.'));
}
$signature = substr($signature, strlen($prefix));
$input = self::encodeRequestDataForSignature(
$method,
$params,
$meta);
$signature = base64_decode($signature);
$trap = new PhutilErrorTrap();
$result = @openssl_verify(
$input,
$signature,
$openssl_public_key);
$err = $trap->getErrorsAsString();
$trap->destroy();
if ($result === 1) {
// Signature is good.
return true;
} else if ($result === 0) {
// Signature is bad.
throw new Exception(
pht(
'Request signature verification failed: signature is not correct.'));
} else {
// Some kind of error.
if (strlen($err)) {
throw new Exception(
pht(
'OpenSSL encountered an error verifying the request signature: %s',
$err));
} else {
throw new Exception(
pht(
'OpenSSL encountered an unknown error verifying the request: %s',
$err));
}
}
}
private static function encodeRequestDataForSignature(
$method,
array $params,
array $meta) {
unset($meta['auth.signature']);
$structure = array(
'method' => $method,
'protocol' => $meta,
'parameters' => $params,
);
return self::encodeRawDataForSignature($structure);
}
public static function encodeRawDataForSignature($data) {
$out = array();
if (is_array($data)) {
if (phutil_is_natural_list($data)) {
$out[] = 'A';
$out[] = count($data);
$out[] = ':';
foreach ($data as $value) {
$out[] = self::encodeRawDataForSignature($value);
}
} else {
ksort($data);
$out[] = 'O';
$out[] = count($data);
$out[] = ':';
foreach ($data as $key => $value) {
$out[] = self::encodeRawDataForSignature($key);
$out[] = self::encodeRawDataForSignature($value);
}
}
} else if (is_string($data)) {
$out[] = 'S';
$out[] = strlen($data);
$out[] = ':';
$out[] = $data;
} else if (is_int($data)) {
$out[] = 'I';
$out[] = strlen((string)$data);
$out[] = ':';
$out[] = (string)$data;
} else if (is_null($data)) {
$out[] = 'N';
$out[] = ':';
} else if ($data === true) {
$out[] = 'B1:';
} else if ($data === false) {
$out[] = 'B0:';
} else {
throw new Exception(
pht(
'Unexpected data type in request data: %s.',
gettype($data)));
}
return implode('', $out);
}
}

View file

@ -0,0 +1,16 @@
<?php
final class ConduitClientException extends Exception {
protected $errorCode;
public function __construct($code, $info) {
parent::__construct("{$code}: {$info}");
$this->errorCode = $code;
}
public function getErrorCode() {
return $this->errorCode;
}
}

View file

@ -0,0 +1,76 @@
<?php
final class ConduitFuture extends FutureProxy {
private $client;
private $conduitMethod;
private $profilerCallID;
public function setClient(ConduitClient $client, $method) {
$this->client = $client;
$this->conduitMethod = $method;
return $this;
}
public function isReady() {
if ($this->profilerCallID === null) {
$profiler = PhutilServiceProfiler::getInstance();
$this->profilerCallID = $profiler->beginServiceCall(
array(
'type' => 'conduit',
'method' => $this->conduitMethod,
'size' => $this->getProxiedFuture()->getHTTPRequestByteLength(),
));
}
return parent::isReady();
}
protected function didReceiveResult($result) {
if ($this->profilerCallID !== null) {
$profiler = PhutilServiceProfiler::getInstance();
$profiler->endServiceCall(
$this->profilerCallID,
array());
}
list($status, $body, $headers) = $result;
if ($status->isError()) {
throw $status;
}
$raw = $body;
$shield = 'for(;;);';
if (!strncmp($raw, $shield, strlen($shield))) {
$raw = substr($raw, strlen($shield));
}
$data = null;
try {
$data = phutil_json_decode($raw);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht(
'Host returned HTTP/200, but invalid JSON data in response to '.
'a Conduit method call.'),
$ex);
}
if ($data['error_code']) {
throw new ConduitClientException(
$data['error_code'],
$data['error_info']);
}
$result = $data['result'];
$result = $this->client->didReceiveResponse(
$this->conduitMethod,
$result);
return $result;
}
}

View file

@ -0,0 +1,34 @@
<?php
final class ConduitClientTestCase extends PhutilTestCase {
public function testConduitRequestEncoding() {
$input = array(
'z' => array(
'nothing' => null,
'emptystring' => '',
),
'empty' => array(
),
'list' => array(
15,
'quack',
true,
false,
),
'a' => array(
'key' => 'value',
'key2' => 'value2',
),
);
$expect =
'O4:S1:aO2:S3:keyS5:valueS4:key2S6:value2S5:emptyA0:S4:listA4:I2:15'.
'S5:quackB1:B0:S1:zO2:S11:emptystringS0:S7:nothingN:';
$this->assertEqual(
$expect,
ConduitClient::encodeRawDataForSignature($input));
}
}

View file

@ -0,0 +1,295 @@
<?php
/**
* Provides access to the command-line console. Instead of reading from or
* writing to stdin/stdout/stderr directly, this class provides a richer API
* including support for ANSI color and formatting, convenience methods for
* prompting the user, and the ability to interact with stdin/stdout/stderr
* in some other process instead of this one.
*
* @task construct Construction
* @task interface Interfacing with the User
* @task internal Internals
*/
final class PhutilConsole extends Phobject {
private static $console;
private $server;
private $channel;
private $messages = array();
private $flushing = false;
private $disabledTypes;
/* -( Console Construction )----------------------------------------------- */
/**
* Use @{method:newLocalConsole} or @{method:newRemoteConsole} to construct
* new consoles.
*
* @task construct
*/
private function __construct() {
$this->disabledTypes = new PhutilArrayWithDefaultValue();
}
/**
* Get the current console. If there's no active console, a new local console
* is created (see @{method:newLocalConsole} for details). You can change the
* active console with @{method:setConsole}.
*
* @return PhutilConsole Active console.
* @task construct
*/
public static function getConsole() {
if (empty(self::$console)) {
self::setConsole(self::newLocalConsole());
}
return self::$console;
}
/**
* Set the active console.
*
* @param PhutilConsole
* @return void
* @task construct
*/
public static function setConsole(PhutilConsole $console) {
self::$console = $console;
}
/**
* Create a new console attached to stdin/stdout/stderr of this process.
* This is how consoles normally work -- for instance, writing output with
* @{method:writeOut} prints directly to stdout. If you don't create a
* console explicitly, a new local console is created for you.
*
* @return PhutilConsole A new console which operates on the pipes of this
* process.
* @task construct
*/
public static function newLocalConsole() {
return self::newConsoleForServer(new PhutilConsoleServer());
}
public static function newConsoleForServer(PhutilConsoleServer $server) {
$console = new PhutilConsole();
$console->server = $server;
return $console;
}
public static function newRemoteConsole() {
$io_channel = new PhutilSocketChannel(
fopen('php://stdin', 'r'),
fopen('php://stdout', 'w'));
$protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel);
$console = new PhutilConsole();
$console->channel = $protocol_channel;
return $console;
}
/* -( Interfacing with the User )------------------------------------------ */
public function confirm($prompt, $default = false) {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_CONFIRM)
->setData(
array(
'prompt' => $prompt,
'default' => $default,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function prompt($prompt, $history = '') {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_PROMPT)
->setData(
array(
'prompt' => $prompt,
'history' => $history,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function sendMessage($data) {
$message = id(new PhutilConsoleMessage())->setData($data);
return $this->writeMessage($message);
}
public function writeOut($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_OUT, $args);
}
public function writeErr($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_ERR, $args);
}
public function writeLog($pattern /* , ... */) {
$args = func_get_args();
return $this->writeTextMessage(PhutilConsoleMessage::TYPE_LOG, $args);
}
public function beginRedirectOut() {
// We need as small buffer as possible. 0 means infinite, 1 means 4096 in
// PHP < 5.4.0.
ob_start(array($this, 'redirectOutCallback'), 2);
$this->flushing = true;
}
public function endRedirectOut() {
$this->flushing = false;
ob_end_flush();
}
/* -( Internals )---------------------------------------------------------- */
// Must be public because it is called from output buffering.
public function redirectOutCallback($string) {
if (strlen($string)) {
$this->flushing = false;
$this->writeOut('%s', $string);
$this->flushing = true;
}
return '';
}
private function writeTextMessage($type, array $argv) {
$message = id(new PhutilConsoleMessage())
->setType($type)
->setData($argv);
$this->writeMessage($message);
return $this;
}
private function writeMessage(PhutilConsoleMessage $message) {
if ($this->disabledTypes[$message->getType()]) {
return $this;
}
if ($this->flushing) {
ob_flush();
}
if ($this->channel) {
$this->channel->write($message);
$this->channel->flush();
} else {
$response = $this->server->handleMessage($message);
if ($response) {
$this->messages[] = $response;
}
}
return $this;
}
private function waitForMessage() {
if ($this->channel) {
$message = $this->channel->waitForMessage();
} else if ($this->messages) {
$message = array_shift($this->messages);
} else {
throw new Exception(
pht(
'%s called with no messages!',
__FUNCTION__.'()'));
}
return $message;
}
public function getServer() {
return $this->server;
}
private function disableMessageType($type) {
$this->disabledTypes[$type] += 1;
return $this;
}
private function enableMessageType($type) {
if ($this->disabledTypes[$type] == 0) {
throw new Exception(pht("Message type '%s' is already enabled!", $type));
}
$this->disabledTypes[$type] -= 1;
return $this;
}
public function disableOut() {
return $this->disableMessageType(PhutilConsoleMessage::TYPE_OUT);
}
public function enableOut() {
return $this->enableMessageType(PhutilConsoleMessage::TYPE_OUT);
}
public function isLogEnabled() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_ENABLED)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_LOG,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function isErrATTY() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_TTY)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_ERR,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
public function getErrCols() {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_COLS)
->setData(
array(
'which' => PhutilConsoleMessage::TYPE_ERR,
));
$this->writeMessage($message);
$response = $this->waitForMessage();
return $response->getData();
}
}

View file

@ -0,0 +1,98 @@
<?php
final class PhutilConsoleFormatter extends Phobject {
private static $colorCodes = array(
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7,
'default' => 9,
);
private static $disableANSI;
public static function disableANSI($disable) {
self::$disableANSI = $disable;
}
public static function getDisableANSI() {
if (self::$disableANSI === null) {
$term = phutil_utf8_strtolower(getenv('TERM'));
// ansicon enables ANSI support on Windows
if (!$term && getenv('ANSICON')) {
$term = 'ansi';
}
if (phutil_is_windows() && $term !== 'cygwin' && $term !== 'ansi') {
self::$disableANSI = true;
} else if (!defined('STDOUT')) {
self::$disableANSI = true;
} else if (function_exists('posix_isatty') && !posix_isatty(STDOUT)) {
self::$disableANSI = true;
} else {
self::$disableANSI = false;
}
}
return self::$disableANSI;
}
public static function formatString($format /* ... */) {
$args = func_get_args();
$args[0] = self::interpretFormat($args[0]);
return call_user_func_array('sprintf', $args);
}
public static function replaceColorCode($matches) {
$codes = self::$colorCodes;
$offset = 30 + $codes[$matches[2]];
$default = 39;
if ($matches[1] == 'bg') {
$offset += 10;
$default += 10;
}
return chr(27).'['.$offset.'m'.$matches[3].chr(27).'['.$default.'m';
}
public static function interpretFormat($format) {
$colors = implode('|', array_keys(self::$colorCodes));
// Sequence should be preceded by start-of-string or non-backslash
// escaping.
$bold_re = '/(?<![\\\\])\*\*(.*)\*\*/sU';
$underline_re = '/(?<![\\\\])__(.*)__/sU';
$invert_re = '/(?<![\\\\])##(.*)##/sU';
if (self::getDisableANSI()) {
$format = preg_replace($bold_re, '\1', $format);
$format = preg_replace($underline_re, '\1', $format);
$format = preg_replace($invert_re, '\1', $format);
$format = preg_replace(
'@<(fg|bg):('.$colors.')>(.*)</\1>@sU',
'\3',
$format);
} else {
$esc = chr(27);
$bold = $esc.'[1m'.'\\1'.$esc.'[m';
$underline = $esc.'[4m'.'\\1'.$esc.'[m';
$invert = $esc.'[7m'.'\\1'.$esc.'[m';
$format = preg_replace($bold_re, $bold, $format);
$format = preg_replace($underline_re, $underline, $format);
$format = preg_replace($invert_re, $invert, $format);
$format = preg_replace_callback(
'@<(fg|bg):('.$colors.')>(.*)</\1>@sU',
array(__CLASS__, 'replaceColorCode'),
$format);
}
// Remove backslash escaping
return preg_replace('/\\\\(\*\*.*\*\*|__.*__|##.*##)/sU', '\1', $format);
}
}

View file

@ -0,0 +1,39 @@
<?php
final class PhutilConsoleMessage extends Phobject {
const TYPE_CONFIRM = 'phutil:confirm';
const TYPE_PROMPT = 'phutil:prompt';
const TYPE_INPUT = 'phutil:in';
const TYPE_OUT = 'phutil:out';
const TYPE_ERR = 'phutil:err';
const TYPE_LOG = 'phutil:log';
const TYPE_TTY = 'phutil:tty?';
const TYPE_IS_TTY = 'phutil:tty!';
const TYPE_COLS = 'phutil:cols?';
const TYPE_COL_WIDTH = 'phutil:cols!';
const TYPE_ENABLED = 'phutil:enabled?';
const TYPE_IS_ENABLED = 'phutil:enabled!';
private $type;
private $data;
public function setData($data) {
$this->data = $data;
return $this;
}
public function getData() {
return $this->data;
}
public function setType($type) {
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
}

View file

@ -0,0 +1,65 @@
<?php
final class PhutilConsoleMetrics extends Phobject {
const DEFAULT_CONSOLE = 'default';
private static $consoles = array();
private $width = false;
public static function getNamedConsole($key) {
if (!isset(self::$consoles[$key])) {
self::$consoles[$key] = new self();
}
return self::$consoles[$key];
}
public static function getDefaultConsole() {
return self::getNamedConsole(self::DEFAULT_CONSOLE);
}
public function didGetWINCHSignal() {
// When we receive a "WINCH" ("WINdow CHange") signal, clear the cached
// information we have about the terminal.
$this->width = false;
return $this;
}
public function getTerminalWidth() {
if ($this->width === false) {
$this->width = $this->computeTerminalWidth();
}
return $this->width;
}
private function computeTerminalWidth() {
if (phutil_is_windows()) {
// TODO: Figure out how to do this on Windows.
return null;
}
$tmp = new TempFile();
// NOTE: We can't just execute this because it won't be connected to a TTY
// if we do.
$err = id(new PhutilExecPassthru('tput cols > %s', $tmp))
->resolve();
$stdout = Filesystem::readFile($tmp);
unset($tmp);
if ($err) {
return null;
}
$width = (int)trim($stdout);
if ($width > 0) {
return $width;
}
return null;
}
}

View file

@ -0,0 +1,179 @@
<?php
/**
* Show a progress bar on the console. Usage:
*
* // Create a progress bar, and configure the total amount of work that
* // needs to be done.
* $bar = id(new PhutilConsoleProgressBar())
* ->setTotal(count($stuff));
*
* // As you complete the work, update the progress bar.
* foreach ($stuff as $thing) {
* do_stuff($thing);
* $bar->update(1);
* }
*
* // When complete, mark the work done to clear the bar.
* $bar->done();
*
* The progress bar attempts to account for various special cases, notably:
*
* - If stderr is not a TTY, the bar will not be drawn (for example, if
* it is being piped to a log file).
* - If the Phutil log output is enabled (usually because `--trace` was
* specified), the bar will not be drawn.
* - The bar will be resized to the width of the console if possible.
*
*/
final class PhutilConsoleProgressBar extends Phobject {
private $work;
private $done;
private $drawn;
private $console;
private $finished;
private $lastUpdate;
private $quiet = false;
public function setConsole(PhutilConsole $console) {
$this->console = $console;
return $this;
}
private function getConsole() {
if ($this->console) {
return $this->console;
}
return PhutilConsole::getConsole();
}
public function setTotal($work) {
$this->work = $work;
$this->redraw();
return $this;
}
public function setQuiet($quiet) {
$this->quiet = $quiet;
return $this;
}
public function update($work) {
$this->done += $work;
$this->redraw();
return $this;
}
private function redraw() {
if ($this->lastUpdate + 0.1 > microtime(true)) {
// We redrew the bar very recently; skip this update.
return $this;
}
return $this->draw();
}
/**
* Explicitly redraw the bar.
*
* Normally, the progress bar is automatically redrawn periodically, but
* you may want to force it to draw.
*
* For example, we force a draw after pre-filling the bar when resuming
* large file uploads in `arc upload`. Otherwise, the bar may sit at 0%
* until the first chunk completes.
*/
public function draw() {
if ($this->quiet) {
return;
}
if ($this->finished) {
return;
}
if (!$this->work) {
// There's no work to be done, so don't draw the bar.
return;
}
$console = $this->getConsole();
if ($console->isErrATTY() === false) {
return;
}
if ($console->isLogEnabled()) {
return;
}
// Width of the stuff other than the progress bar itself.
$chrome_width = strlen('[] 100.0% ');
$char_width = $this->getWidth();
if ($char_width < $chrome_width) {
return;
}
$this->lastUpdate = microtime(true);
if (!$this->drawn) {
$this->drawn = true;
}
$percent = $this->done / $this->work;
$max_width = $char_width - $chrome_width;
$bar_width = $percent * $max_width;
$bar_int = floor($bar_width);
$bar_frac = $bar_width - $bar_int;
$frac_map = array(
'',
'-',
'~',
);
$frac_char = $frac_map[floor($bar_frac * count($frac_map))];
$pattern = "[%-{$max_width}.{$max_width}s] % 5s%%";
$out = sprintf(
$pattern,
str_repeat('=', $bar_int).$frac_char,
sprintf('%.1f', 100 * $percent));
$this->eraseLine();
$console->writeErr('%s', $out);
return $this;
}
public function done($clean_exit = true) {
$console = $this->getConsole();
if ($this->drawn) {
$this->eraseLine();
if ($clean_exit) {
$console->writeErr("%s\n", pht('Done.'));
}
}
$this->finished = true;
}
private function eraseLine() {
$string = str_repeat(' ', $this->getWidth());
$console = $this->getConsole();
$console->writeErr("\r%s\r", $string);
}
private function getWidth() {
$console = $this->getConsole();
$width = $console->getErrCols();
return min(nonempty($width, 78), 78);
}
public function __destruct() {
$this->done($clean_exit = false);
}
}

View file

@ -0,0 +1,158 @@
<?php
final class PhutilConsoleServer extends Phobject {
private $clients = array();
private $handler;
private $enableLog;
public function handleMessage(PhutilConsoleMessage $message) {
$data = $message->getData();
$type = $message->getType();
switch ($type) {
case PhutilConsoleMessage::TYPE_CONFIRM:
$ok = phutil_console_confirm($data['prompt'], !$data['default']);
return $this->buildMessage(
PhutilConsoleMessage::TYPE_INPUT,
$ok);
case PhutilConsoleMessage::TYPE_PROMPT:
$response = phutil_console_prompt(
$data['prompt'],
idx($data, 'history'));
return $this->buildMessage(
PhutilConsoleMessage::TYPE_INPUT,
$response);
case PhutilConsoleMessage::TYPE_OUT:
$this->writeText(STDOUT, $data);
return null;
case PhutilConsoleMessage::TYPE_ERR:
$this->writeText(STDERR, $data);
return null;
case PhutilConsoleMessage::TYPE_LOG:
if ($this->enableLog) {
$this->writeText(STDERR, $data);
}
return null;
case PhutilConsoleMessage::TYPE_ENABLED:
switch ($data['which']) {
case PhutilConsoleMessage::TYPE_LOG:
$enabled = $this->enableLog;
break;
default:
$enabled = true;
break;
}
return $this->buildMessage(
PhutilConsoleMessage::TYPE_IS_ENABLED,
$enabled);
case PhutilConsoleMessage::TYPE_TTY:
case PhutilConsoleMessage::TYPE_COLS:
switch ($data['which']) {
case PhutilConsoleMessage::TYPE_OUT:
$which = STDOUT;
break;
case PhutilConsoleMessage::TYPE_ERR:
$which = STDERR;
break;
}
switch ($type) {
case PhutilConsoleMessage::TYPE_TTY:
if (function_exists('posix_isatty')) {
$is_a_tty = posix_isatty($which);
} else {
$is_a_tty = null;
}
return $this->buildMessage(
PhutilConsoleMessage::TYPE_IS_TTY,
$is_a_tty);
case PhutilConsoleMessage::TYPE_COLS:
// TODO: This is an approximation which might not be perfectly
// accurate.
$width = phutil_console_get_terminal_width();
return $this->buildMessage(
PhutilConsoleMessage::TYPE_COL_WIDTH,
$width);
}
break;
default:
if ($this->handler) {
return call_user_func($this->handler, $message);
} else {
throw new Exception(
pht(
"Received unknown console message of type '%s'.",
$type));
}
}
}
/**
* Set handler called for unknown messages.
*
* @param callable Signature: (PhutilConsoleMessage $message).
*/
public function setHandler($callback) {
$this->handler = $callback;
return $this;
}
private function buildMessage($type, $data) {
$response = new PhutilConsoleMessage();
$response->setType($type);
$response->setData($data);
return $response;
}
public function addExecFutureClient(ExecFuture $future) {
$io_channel = new PhutilExecChannel($future);
$protocol_channel = new PhutilPHPObjectProtocolChannel($io_channel);
$server_channel = new PhutilConsoleServerChannel($protocol_channel);
$io_channel->setStderrHandler(array($server_channel, 'didReceiveStderr'));
return $this->addClient($server_channel);
}
public function addClient(PhutilConsoleServerChannel $channel) {
$this->clients[] = $channel;
return $this;
}
public function setEnableLog($enable) {
$this->enableLog = $enable;
return $this;
}
public function run() {
while ($this->clients) {
PhutilChannel::waitForAny($this->clients);
foreach ($this->clients as $key => $client) {
if (!$client->update()) {
// If the client has exited, remove it from the list of clients.
// We still need to process any remaining buffered I/O.
unset($this->clients[$key]);
}
while ($message = $client->read()) {
$response = $this->handleMessage($message);
if ($response) {
$client->write($response);
}
}
}
}
}
private function writeText($where, array $argv) {
$text = call_user_func_array('phutil_console_format', $argv);
fprintf($where, '%s', $text);
}
}

View file

@ -0,0 +1,12 @@
<?php
final class PhutilConsoleServerChannel extends PhutilChannelChannel {
public function didReceiveStderr(PhutilExecChannel $channel, $stderr) {
$message = id(new PhutilConsoleMessage())
->setType(PhutilConsoleMessage::TYPE_ERR)
->setData(array('%s', $stderr));
$this->getUnderlyingChannel()->addMessage($message);
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* Thrown when you prompt the user with @{function:phutil_console_prompt} or
* @{function:phutil_console_confirm} but stdin is not an interactive TTY so
* the user can't possibly respond. Usually this means the user ran the command
* with something piped into stdin.
*/
final class PhutilConsoleStdinNotInteractiveException extends Exception {
public function __construct() {
parent::__construct(
pht(
'The program is attempting to read user input, but stdin is being '.
'piped from some other source (not a TTY).'));
}
}

View file

@ -0,0 +1,308 @@
<?php
/**
* Edit a document interactively, by launching $EDITOR (like vi or nano).
*
* $result = id(new InteractiveEditor($document))
* ->setName('shopping_list')
* ->setLineOffset(15)
* ->editInteractively();
*
* This will launch the user's $EDITOR to edit the specified '$document', and
* return their changes into '$result'.
*
* @task create Creating a New Editor
* @task edit Editing Interactively
* @task config Configuring Options
*/
final class PhutilInteractiveEditor extends Phobject {
private $name = '';
private $content = '';
private $offset = 0;
private $preferred;
private $fallback;
/* -( Creating a New Editor )---------------------------------------------- */
/**
* Constructs an interactive editor, using the text of a document.
*
* @param string Document text.
* @return $this
*
* @task create
*/
public function __construct($content) {
$this->setContent($content);
}
/* -( Editing Interactively )----------------------------------------------- */
/**
* Launch an editor and edit the content. The edited content will be
* returned.
*
* @return string Edited content.
* @throws Exception The editor exited abnormally or something untoward
* occurred.
*
* @task edit
*/
public function editInteractively() {
$name = $this->getName();
$content = $this->getContent();
if (phutil_is_windows()) {
$content = str_replace("\n", "\r\n", $content);
}
$tmp = Filesystem::createTemporaryDirectory('edit.');
$path = $tmp.DIRECTORY_SEPARATOR.$name;
try {
Filesystem::writeFile($path, $content);
} catch (Exception $ex) {
Filesystem::remove($tmp);
throw $ex;
}
$editor = $this->getEditor();
$offset = $this->getLineOffset();
$err = $this->invokeEditor($editor, $path, $offset);
if ($err) {
// See T13297. On macOS, "vi" and "vim" may exit with errors even though
// the edit succeeded. If the binary is "vi" or "vim" and we get an exit
// code, we perform an additional test on the binary.
$vi_binaries = array(
'vi' => true,
'vim' => true,
);
$binary = basename($editor);
if (isset($vi_binaries[$binary])) {
// This runs "Q" (an invalid command), then "q" (a valid command,
// meaning "quit"). Vim binaries with behavior that makes them poor
// interactive editors will exit "1".
list($diagnostic_err) = exec_manual('%R +Q +q', $binary);
// If we get an error back, the binary is badly behaved. Ignore the
// original error and assume it's not meaningful, since it just
// indicates the user made a typo in a command when editing
// interactively, which is routine and unconcerning.
if ($diagnostic_err) {
$err = 0;
}
}
}
if ($err) {
Filesystem::remove($tmp);
throw new Exception(pht('Editor exited with an error code (#%d).', $err));
}
try {
$result = Filesystem::readFile($path);
Filesystem::remove($tmp);
} catch (Exception $ex) {
Filesystem::remove($tmp);
throw $ex;
}
if (phutil_is_windows()) {
$result = str_replace("\r\n", "\n", $result);
}
$this->setContent($result);
return $this->getContent();
}
private function invokeEditor($editor, $path, $offset) {
// NOTE: Popular Windows editors like Notepad++ and GitPad do not support
// line offsets, so just ignore the offset feature on Windows. We rarely
// use it anyway.
$offset_flag = '';
if ($offset && !phutil_is_windows()) {
$offset = (int)$offset;
if (preg_match('/^mate/', $editor)) {
$offset_flag = csprintf('-l %d', $offset);
} else {
$offset_flag = csprintf('+%d', $offset);
}
}
$cmd = csprintf(
'%C %C %s',
$editor,
$offset_flag,
$path);
return phutil_passthru('%C', $cmd);
}
/* -( Configuring Options )------------------------------------------------- */
/**
* Set the line offset where the cursor should be positioned when the editor
* opens. By default, the cursor will be positioned at the start of the
* content.
*
* @param int Line number where the cursor should be positioned.
* @return $this
*
* @task config
*/
public function setLineOffset($offset) {
$this->offset = (int)$offset;
return $this;
}
/**
* Get the current line offset. See setLineOffset().
*
* @return int Current line offset.
*
* @task config
*/
public function getLineOffset() {
return $this->offset;
}
/**
* Set the document name. Depending on the editor, this may be exposed to
* the user and can give them a sense of what they're editing.
*
* @param string Document name.
* @return $this
*
* @task config
*/
public function setName($name) {
$name = preg_replace('/[^A-Z0-9._-]+/i', '', $name);
$this->name = $name;
return $this;
}
/**
* Get the current document name. See @{method:setName} for details.
*
* @return string Current document name.
*
* @task config
*/
public function getName() {
if (!strlen($this->name)) {
return 'untitled';
}
return $this->name;
}
/**
* Set the text content to be edited.
*
* @param string New content.
* @return $this
*
* @task config
*/
public function setContent($content) {
$this->content = $content;
return $this;
}
/**
* Retrieve the current content.
*
* @return string
*
* @task config
*/
public function getContent() {
return $this->content;
}
/**
* Set the fallback editor program to be used if the env variable $EDITOR
* is not available and there is no `editor` binary in PATH.
*
* @param string Command-line editing program (e.g. 'emacs', 'vi')
* @return $this
*
* @task config
*/
public function setFallbackEditor($editor) {
$this->fallback = $editor;
return $this;
}
/**
* Set the preferred editor program. If set, this will override all other
* sources of editor configuration, like $EDITOR.
*
* @param string Command-line editing program (e.g. 'emacs', 'vi')
* @return $this
*
* @task config
*/
public function setPreferredEditor($editor) {
$this->preferred = $editor;
return $this;
}
/**
* Get the name of the editor program to use. The value of the environmental
* variable $EDITOR will be used if available; otherwise, the `editor` binary
* if present; otherwise the best editor will be selected.
*
* @return string Command-line editing program.
*
* @task config
*/
public function getEditor() {
if ($this->preferred) {
return $this->preferred;
}
$editor = getenv('EDITOR');
if ($editor) {
return $editor;
}
if ($this->fallback) {
return $this->fallback;
}
$candidates = array('editor', 'nano', 'sensible-editor', 'vi');
foreach ($candidates as $cmd) {
if (Filesystem::binaryExists($cmd)) {
return $cmd;
}
}
throw new Exception(
pht(
'Unable to launch an interactive text editor. Set the %s '.
'environment variable to an appropriate editor.',
'EDITOR'));
}
}

View file

@ -0,0 +1,48 @@
<?php
final class PhutilConsoleWrapTestCase extends PhutilTestCase {
public function testWrap() {
$dir = dirname(__FILE__).'/wrap/';
$files = Filesystem::listDirectory($dir);
foreach ($files as $file) {
if (preg_match('/.txt$/', $file)) {
$this->assertEqual(
Filesystem::readFile($dir.$file.'.expect'),
phutil_console_wrap(Filesystem::readFile($dir.$file)),
$file);
}
}
}
public function testConsoleWrap() {
$this->assertEqual(
phutil_console_format(
"<bg:red>** %s **</bg> abc abc abc abc abc abc abc abc abc abc ".
"abc abc abc abc abc abc abc\nabc abc abc abc abc abc abc abc abc ".
"abc abc!",
pht('ERROR')),
phutil_console_wrap(
phutil_console_format(
'<bg:red>** %s **</bg> abc abc abc abc abc abc abc abc abc abc '.
'abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc '.
'abc abc!',
pht('ERROR'))),
pht('ANSI escape sequences should not contribute toward wrap width.'));
}
public function testWrapIndent() {
$turtles = <<<EOTURTLES
turtle turtle turtle turtle turtle turtle turtle turtle
turtle turtle turtle turtle turtle turtle turtle turtle
turtle turtle turtle turtle
EOTURTLES;
$this->assertEqual(
$turtles,
phutil_console_wrap(
rtrim(str_repeat('turtle ', 20)),
$indent = 20));
}
}

View file

@ -0,0 +1 @@
Say MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM every day.

View file

@ -0,0 +1,3 @@
Say
MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
every day.

View file

@ -0,0 +1,10 @@
Curabitur gravida lectus odio, nec dictum sapien.
Donec condimentum purus at est aliquam lobortis.
Sed facilisis justo a purus interdum at venenatis eros laoreet.
Quisque ac odio vitae erat congue elementum.
Etiam semper venenatis massa vitae faucibus.
Praesent eget eros tortor.
Vestibulum in pharetra massa.
Integer risus justo, malesuada auctor feugiat venenatis, viverra iaculis est.
Praesent a tortor et dui tempus egestas.
Sed lacinia diam id velit tincidunt sagittis.

View file

@ -0,0 +1,10 @@
Curabitur gravida lectus odio, nec dictum sapien.
Donec condimentum purus at est aliquam lobortis.
Sed facilisis justo a purus interdum at venenatis eros laoreet.
Quisque ac odio vitae erat congue elementum.
Etiam semper venenatis massa vitae faucibus.
Praesent eget eros tortor.
Vestibulum in pharetra massa.
Integer risus justo, malesuada auctor feugiat venenatis, viverra iaculis est.
Praesent a tortor et dui tempus egestas.
Sed lacinia diam id velit tincidunt sagittis.

View file

@ -0,0 +1 @@
Morbi auctor commodo libero, vel interdum leo commodo nec. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum dictum pretium lorem ac commodo. Vivamus ullamcorper neque et velit interdum ornare. Fusce adipiscing metus non sem porttitor scelerisque. Aliquam mattis sem non tortor semper eget fermentum libero faucibus. Nam vulputate mauris at nunc bibendum mollis. Aliquam mattis rutrum turpis a fringilla. Mauris quis nulla eget nunc mollis pharetra id sit amet arcu. Nam ut urna in ligula facilisis scelerisque in nec massa. Morbi posuere, turpis in bibendum fringilla, augue felis gravida est, vitae convallis quam nunc at tellus.

View file

@ -0,0 +1,9 @@
Morbi auctor commodo libero, vel interdum leo commodo nec. Cum sociis natoque
penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum
dictum pretium lorem ac commodo. Vivamus ullamcorper neque et velit interdum
ornare. Fusce adipiscing metus non sem porttitor scelerisque. Aliquam mattis
sem non tortor semper eget fermentum libero faucibus. Nam vulputate mauris at
nunc bibendum mollis. Aliquam mattis rutrum turpis a fringilla. Mauris quis
nulla eget nunc mollis pharetra id sit amet arcu. Nam ut urna in ligula
facilisis scelerisque in nec massa. Morbi posuere, turpis in bibendum
fringilla, augue felis gravida est, vitae convallis quam nunc at tellus.

View file

@ -0,0 +1 @@
Do you want to do stuff? [y/N]

View file

@ -0,0 +1 @@
Do you want to do stuff? [y/N]

View file

@ -0,0 +1 @@
☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃

View file

@ -0,0 +1,2 @@
☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃
☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃ ☃☃☃☃☃

209
src/console/format.php Normal file
View file

@ -0,0 +1,209 @@
<?php
function phutil_console_format($format /* ... */) {
$args = func_get_args();
return call_user_func_array(
array('PhutilConsoleFormatter', 'formatString'),
$args);
}
function phutil_console_confirm($prompt, $default_no = true) {
$prompt_options = $default_no ? '[y/N]' : '[Y/n]';
do {
$response = phutil_console_prompt($prompt.' '.$prompt_options);
$c = trim(strtolower($response));
} while ($c != 'y' && $c != 'n' && $c != '');
echo "\n";
if ($default_no) {
return ($c == 'y');
} else {
return ($c != 'n');
}
}
function phutil_console_select($prompt, $min, $max) {
$select_options = '['.$min.' - '.$max.']';
do {
$response = phutil_console_prompt($prompt.' '.$select_options);
$selection = trim($response);
if (preg_match('/^\d+\z/', $selection)) {
$selection = (int)$selection;
if ($selection >= $min && $selection <= $max) {
return $selection;
}
}
} while (true);
}
function phutil_console_prompt($prompt, $history = '') {
echo "\n\n";
$prompt = phutil_console_wrap($prompt.' ', 4);
try {
phutil_console_require_tty();
} catch (PhutilConsoleStdinNotInteractiveException $ex) {
// Throw after echoing the prompt so the user has some idea what happened.
echo $prompt;
throw $ex;
}
// `escapeshellarg` makes double quotes in the command below disappear on
// Windows, which breaks prompts when using history. See T6348
$use_history = !phutil_is_windows();
if ($history == '') {
$use_history = false;
} else {
// Test if bash is available by seeing if it can run `true`.
list($err) = exec_manual('bash -c %s', 'true');
if ($err) {
$use_history = false;
}
}
if (!$use_history) {
echo $prompt;
$response = fgets(STDIN);
} else {
// There's around 0% chance that readline() is available directly in PHP,
// so we're using bash/read/history instead.
$command = csprintf(
'bash -c %s',
csprintf(
'history -r %s 2>/dev/null; '.
'read -e -p %s; '.
'echo "$REPLY"; '.
'history -s "$REPLY" 2>/dev/null; '.
'history -w %s 2>/dev/null',
$history,
$prompt,
$history));
// execx() doesn't work with input, phutil_passthru() doesn't return output.
$response = shell_exec($command);
}
return rtrim($response, "\r\n");
}
/**
* Soft wrap text for display on a console, respecting UTF8 character boundaries
* and ANSI color escape sequences.
*
* @param string Text to wrap.
* @param int Optional indent level.
* @param bool True to also indent the first line.
* @return string Wrapped text.
*/
function phutil_console_wrap($text, $indent = 0, $with_prefix = true) {
$lines = array();
$width = (78 - $indent);
$esc = chr(27);
$break_pos = null;
$len_after_break = 0;
$line_len = 0;
$line = array();
$lines = array();
$vector = phutil_utf8v($text);
$vector_len = count($vector);
for ($ii = 0; $ii < $vector_len; $ii++) {
$chr = $vector[$ii];
// If this is an ANSI escape sequence for a color code, just consume it
// without counting it toward the character limit. This prevents lines
// with bold/color on them from wrapping too early.
if ($chr == $esc) {
for ($ii; $ii < $vector_len; $ii++) {
$line[] = $vector[$ii];
if ($vector[$ii] == 'm') {
break;
}
}
continue;
}
$line[] = $chr;
++$line_len;
++$len_after_break;
if ($line_len > $width) {
if ($break_pos !== null) {
$slice = array_slice($line, 0, $break_pos);
while (count($slice) && end($slice) == ' ') {
array_pop($slice);
}
$slice[] = "\n";
$lines[] = $slice;
$line = array_slice($line, $break_pos);
$line_len = $len_after_break;
$len_after_break = 0;
$break_pos = null;
}
}
if ($chr == ' ') {
$break_pos = count($line);
$len_after_break = 0;
}
if ($chr == "\n") {
$lines[] = $line;
$line = array();
$len_after_break = 0;
$line_len = 0;
$break_pos = null;
}
}
if ($line) {
if ($line) {
$lines[] = $line;
}
}
$pre = null;
if ($indent) {
$pre = str_repeat(' ', $indent);
}
foreach ($lines as $idx => $line) {
if ($idx == 0 && !$with_prefix) {
$prefix = null;
} else {
$prefix = $pre;
}
$lines[$idx] = $prefix.implode('', $line);
}
return implode('', $lines);
}
function phutil_console_require_tty() {
if (function_exists('posix_isatty') && !posix_isatty(STDIN)) {
throw new PhutilConsoleStdinNotInteractiveException();
}
}
/**
* Determine the width of the terminal, if possible. Returns `null` on failure.
*
* @return int|null Terminal width in characters, or null on failure.
*/
function phutil_console_get_terminal_width() {
return PhutilConsoleMetrics::getDefaultConsole()
->getTerminalWidth();
}

View file

@ -0,0 +1,48 @@
<?php
final class PhutilConsoleBlock extends PhutilConsoleView {
private $items = array();
public function addParagraph($item) {
$this->items[] = array(
'type' => 'paragraph',
'item' => $item,
);
return $this;
}
public function addList(PhutilConsoleList $list) {
$this->items[] = array(
'type' => 'list',
'item' => $list,
);
return $this;
}
protected function drawView() {
$output = array();
foreach ($this->items as $spec) {
$type = $spec['type'];
$item = $spec['item'];
switch ($type) {
case 'paragraph':
$item = array(
tsprintf('%s', $item)->applyWrap(),
"\n",
);
break;
case 'list':
$item = $item;
break;
}
$output[] = $item;
}
return $this->drawLines($output);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhutilConsoleError
extends PhutilConsoleLogLine {
protected function getLogLineColor() {
return 'red';
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhutilConsoleInfo
extends PhutilConsoleLogLine {
protected function getLogLineColor() {
return 'green';
}
}

View file

@ -0,0 +1,63 @@
<?php
final class PhutilConsoleList extends PhutilConsoleView {
private $items = array();
private $wrap = true;
private $bullet = '-';
public function addItem($item) {
$this->items[] = $item;
return $this;
}
public function addItems(array $items) {
foreach ($items as $item) {
$this->addItem($item);
}
return $this;
}
public function getItems() {
return $this->items;
}
public function setBullet($bullet) {
$this->bullet = $bullet;
return $this;
}
public function getBullet() {
return $this->bullet;
}
public function setWrap($wrap) {
$this->wrap = $wrap;
return $this;
}
protected function drawView() {
$indent_depth = 6;
$indent_string = str_repeat(' ', $indent_depth);
if ($this->bullet !== null) {
$bullet = $this->bullet.' ';
$indent_depth = $indent_depth + phutil_utf8_console_strlen($bullet);
} else {
$bullet = '';
}
$output = array();
foreach ($this->getItems() as $item) {
if ($this->wrap) {
$item = tsprintf('%s', $item)
->applyIndent($indent_depth, false);
}
$output[] = $indent_string.$bullet.$item;
}
return $this->drawLines($output);
}
}

View file

@ -0,0 +1,24 @@
<?php
abstract class PhutilConsoleLogLine extends PhutilConsoleView {
private $kind;
private $message;
abstract protected function getLogLineColor();
public function __construct($kind, $message) {
$this->kind = $kind;
$this->message = $message;
}
protected function drawView() {
$color = $this->getLogLineColor();
return tsprintf(
"<bg:".$color.">** %s **</bg> %s\n",
$this->kind,
$this->message);
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhutilConsoleSkip
extends PhutilConsoleLogLine {
protected function getLogLineColor() {
return 'blue';
}
}

View file

@ -0,0 +1,296 @@
<?php
/**
* Show a table in the console. Usage:
*
* $table = id(new PhutilConsoleTable())
* ->addColumn('id', array('title' => 'ID', 'align' => 'right'))
* ->addColumn('name', array('title' => 'Username', 'align' => 'center'))
* ->addColumn('email', array('title' => 'Email Address'))
*
* ->addRow(array(
* 'id' => 12345,
* 'name' => 'alicoln',
* 'email' => 'abraham@lincoln.com',
* ))
* ->addRow(array(
* 'id' => 99999999,
* 'name' => 'jbloggs',
* 'email' => 'joe@bloggs.com',
* ))
*
* ->setBorders(true)
* ->draw();
*/
final class PhutilConsoleTable extends PhutilConsoleView {
private $columns = array();
private $data = array();
private $widths = array();
private $borders = false;
private $padding = 1;
private $showHeader = true;
const ALIGN_LEFT = 'left';
const ALIGN_CENTER = 'center';
const ALIGN_RIGHT = 'right';
/* -( Configuration )------------------------------------------------------ */
public function setBorders($borders) {
$this->borders = $borders;
return $this;
}
public function setPadding($padding) {
$this->padding = $padding;
return $this;
}
public function setShowHeader($show_header) {
$this->showHeader = $show_header;
return $this;
}
/* -( Data )--------------------------------------------------------------- */
public function addColumn($key, array $column) {
PhutilTypeSpec::checkMap($column, array(
'title' => 'string',
'align' => 'optional string',
));
$this->columns[$key] = $column;
return $this;
}
public function addColumns(array $columns) {
foreach ($columns as $key => $column) {
$this->addColumn($key, $column);
}
return $this;
}
public function addRow(array $data) {
$this->data[] = $data;
foreach ($data as $key => $value) {
$this->widths[$key] = max(
idx($this->widths, $key, 0),
phutil_utf8_console_strlen($value));
}
return $this;
}
/* -( Drawing )------------------------------------------------------------ */
protected function drawView() {
return $this->drawLines(
array_merge(
$this->getHeader(),
$this->getBody(),
$this->getFooter()));
}
private function getHeader() {
$output = array();
if ($this->borders) {
$output[] = $this->formatSeparator('=');
}
if (!$this->showHeader) {
return $output;
}
$columns = array();
foreach ($this->columns as $key => $column) {
$title = tsprintf('**%s**', $column['title']);
if ($this->shouldAddSpacing($key, $column)) {
$title = $this->alignString(
$title,
$this->getWidth($key),
idx($column, 'align', self::ALIGN_LEFT));
}
$columns[] = $title;
}
$output[] = $this->formatRow($columns);
if ($this->borders) {
$output[] = $this->formatSeparator('=');
}
return $output;
}
private function getBody() {
$output = array();
foreach ($this->data as $data) {
$columns = array();
foreach ($this->columns as $key => $column) {
if (!$this->shouldAddSpacing($key, $column)) {
$columns[] = idx($data, $key, '');
} else {
$columns[] = $this->alignString(
idx($data, $key, ''),
$this->getWidth($key),
idx($column, 'align', self::ALIGN_LEFT));
}
}
$output[] = $this->formatRow($columns);
}
return $output;
}
private function getFooter() {
$output = array();
if ($this->borders) {
$columns = array();
foreach ($this->getColumns() as $column) {
$columns[] = str_repeat('=', $this->getWidth($column));
}
$output[] = array(
'+',
$this->implode('+', $columns),
'+',
);
}
return $output;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Returns if the specified column should have spacing added.
*
* @return bool
*/
private function shouldAddSpacing($key, $column) {
if (!$this->borders) {
if (last_key($this->columns) === $key) {
if (idx($column, 'align', self::ALIGN_LEFT) === self::ALIGN_LEFT) {
// Don't add extra spaces to this column since it's the last column,
// left aligned, and we're not showing borders. This prevents
// unnecessary empty lines from appearing when the extra spaces
// wrap around the terminal.
return false;
}
}
}
return true;
}
/**
* Returns the column IDs.
*
* @return list<string>
*/
protected function getColumns() {
return array_keys($this->columns);
}
/**
* Get the width of a specific column, including padding.
*
* @param string
* @return int
*/
protected function getWidth($key) {
$width = max(
idx($this->widths, $key),
phutil_utf8_console_strlen(
idx(idx($this->columns, $key, array()), 'title', '')));
return $width + 2 * $this->padding;
}
protected function alignString($string, $width, $align) {
$num_padding = $width -
(2 * $this->padding) - phutil_utf8_console_strlen($string);
switch ($align) {
case self::ALIGN_LEFT:
$num_left_padding = 0;
$num_right_padding = $num_padding;
break;
case self::ALIGN_CENTER:
$num_left_padding = (int)($num_padding / 2);
$num_right_padding = $num_padding - $num_left_padding;
break;
case self::ALIGN_RIGHT:
$num_left_padding = $num_padding;
$num_right_padding = 0;
break;
}
$left_padding = str_repeat(' ', $num_left_padding);
$right_padding = str_repeat(' ', $num_right_padding);
return array(
$left_padding,
$string,
$right_padding,
);
}
/**
* Format cells into an entire row.
*
* @param list<string>
* @return string
*/
protected function formatRow(array $columns) {
$padding = str_repeat(' ', $this->padding);
if ($this->borders) {
$separator = $padding.'|'.$padding;
return array(
'|'.$padding,
$this->implode($separator, $columns),
$padding.'|',
);
} else {
return $this->implode($padding, $columns);
}
}
protected function formatSeparator($string) {
$columns = array();
if ($this->borders) {
$separator = '+';
} else {
$separator = '';
}
foreach ($this->getColumns() as $column) {
$columns[] = str_repeat($string, $this->getWidth($column));
}
return array(
$separator,
$this->implode($separator, $columns),
$separator,
);
}
}

View file

@ -0,0 +1,112 @@
<?php
abstract class PhutilConsoleView extends Phobject {
private $console;
abstract protected function drawView();
final public function setConsole(PhutilConsole $console) {
$this->console = $console;
return $this;
}
final public function getConsole() {
if ($this->console) {
return $this->console;
}
return PhutilConsole::getConsole();
}
/**
* Draw a view to the console.
*
* @return this
* @task draw
*/
final public function draw() {
$string = $this->drawConsoleString();
$console = $this->getConsole();
$console->writeOut('%s', $string);
return $this;
}
/**
* Draw a view to a string and return it.
*
* @return string Console-printable string.
* @task draw
*/
final public function drawConsoleString() {
$view = $this->drawView();
$parts = $this->reduceView($view);
$out = array();
foreach ($parts as $part) {
$out[] = PhutilTerminalString::escapeStringValue($part, true);
}
return implode('', $out);
}
/**
* Reduce a view to a list of simple, unnested parts.
*
* @param wild Any drawable view.
* @return list<wild> List of unnested drawables.
* @task draw
*/
private function reduceView($view) {
if ($view instanceof PhutilConsoleView) {
$view = $view->drawView();
return $this->reduceView($view);
}
if (is_array($view)) {
$parts = array();
foreach ($view as $item) {
foreach ($this->reduceView($item) as $part) {
$parts[] = $part;
}
}
return $parts;
}
return array($view);
}
/* -( Drawing Utilities )-------------------------------------------------- */
/**
* @param list<wild> List of views, one per line.
* @return wild Each view rendered on a separate line.
*/
final protected function drawLines(array $parts) {
$result = array();
foreach ($parts as $part) {
if ($part !== null) {
$result[] = $part;
$result[] = "\n";
}
}
return $result;
}
final protected function implode($separator, array $items) {
$result = array();
foreach ($items as $item) {
$result[] = $item;
$result[] = $separator;
}
array_pop($result);
return $result;
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhutilConsoleWarning
extends PhutilConsoleLogLine {
protected function getLogLineColor() {
return 'yellow';
}
}

View file

@ -0,0 +1,8 @@
@title Using Amazon Web Services APIs
@group aws
Don't use them.
= Mega Alpha =
NOTE: These APIs are really sketchy right now.

View file

@ -0,0 +1,64 @@
@title Command Execution
@group exec
This document describes best practices for executing system commands in PHP
using libphutil.
= Overview =
PHP has several built-in mechanisms for executing system commands, like
`exec()`, `system()`, and the backtick operator. However, these mechanisms
often make it difficult to get all the information you need to handle error
conditions, properly escaping commands is cumbersome, and they do not provide
more advanced features like parallel execution and timeouts.
This document describes how to use the APIs in libphutil to execute commands
without encountering these problems.
= Simple Commands: `execx()` and `exec_manual()` =
@{function:execx} and @{function:exec_manual} are replacements for `exec()`,
`system()`, `shell_exec()`, and the backtick operator. The APIs look like this:
list($stdout, $stderr) = execx('ls %s', $path);
list($err, $stdout, $stderr) = exec_manual('ls %s', $path);
The major advantages of these methods over the `exec()` family are that you can
easily check return codes, capture both stdout and stderr, and use a simple
`sprintf()`-style formatting string to properly escape commands.
@{function:execx} will throw a @{class:CommandException} if the command you
execute terminates with a nonzero exit code, while @{function:exec_manual}
returns the error code. If you use @{function:exec_manual}, you must manually
check the error code.
= Advanced Commands: `ExecFutures` =
If you need more advanced features like parallel execution, command timeouts,
and asynchronous I/O, use @{class:ExecFuture}.
$future = new ExecFuture('ls %s', $path);
list($stdout, $stderr) = $future->resolvex();
@{class:ExecFuture} is a @{class:Future}, and can be used with constructs like
@{class:FutureIterator} to achieve and manage parallelism. See
@{article:Using Futures} for general information on how to use futures in
libphutil.
In addition to futures-based parallelism, you can set a timeout on an
@{class:ExecFuture}, which will kill the command if it takes longer than the
specified number of seconds to execute:
$future->setTimeout(30);
If the command runs longer than the timeout, the process will be killed and the
future will resolve with a failure code (`ExecFuture::TIMED_OUT_EXIT_CODE`).
You can also write to the stdin of a process by using the
@{method:ExecFuture::write} method.
$future = new ExecFuture('bc');
$future->write('2+2');
list($stdout) = $future->resolvex();
See @{class:ExecFuture} for complete capability documentation.

View file

@ -0,0 +1,45 @@
@title Core Utilities Quick Reference
@group util
Summary of libphutil core utilities.
= Overview =
This document provides a brief overview of the libphutil core utilities.
= Language Capabilities =
Functions @{function:id}, @{function:head} and @{function:newv} address
language grammar and implementation limitations.
You can efficiently merge a vector of arrays with @{function:array_mergev}.
Functions @{function:head}, @{function:last}, @{function:head_key} and
@{function:last_key} let you access the first or last elements of an array
without raising warnings.
You can combine an array with itself safely with @{function:array_fuse}.
= Default Value Selection =
Functions @{function:idx}, @{function:nonempty} and @{function:coalesce} help
you to select default values when keys or parameters are missing or empty.
= Array and Object Manipulation =
Functions @{function:ipull}, @{function:igroup}, @{function:isort} and
@{function:ifilter} (**i** stands for **index**) simplify common data
manipulations applied to lists of arrays.
Functions @{function:mpull}, @{function:mgroup}, @{function:msort} and
@{function:mfilter} (**m** stands for **method**) provide the same capabilities
for lists of objects.
@{function:array_select_keys} allows you to choose or reorder keys from a
dictionary.
= Lunar Phases =
@{class:PhutilLunarPhase} calculates lunar phases, allowing you to harden an
application against threats from werewolves, werebears, and other
werecreatures.

View file

@ -0,0 +1,17 @@
@title Developing XHPAST
@group xhpast
Instructions for developing XHPAST.
= XHPAST Development Builds =
To develop XHPAST, you need to install flex and bison. These install out of
most package systems, with the caveat that you need flex 2.3.35 (which is NEWER
than flex 2.3.4) and some package systems don't have it yet. If this is the
case for you, you can grab the source here:
http://flex.sourceforge.net/
When building, run `make scanner parser all` instead of `make` to build the
entire toolchain. By default the scanner and parser are not rebuild, to avoid
requiring normal users to install flex and bison.

View file

@ -0,0 +1,57 @@
@title libphutil Overview
@group overview
This document provides a high-level introduction to libphutil.
= Overview =
**libphutil** (pronounced as "lib-futile", like the English word //futile//) is
a collection of PHP utility classes and functions. Most code in the library is
general-purpose, and makes it easier to build applications in PHP.
libphutil is principally the shared library for
[[ http://www.phabricator.org | Phabricator ]] and its CLI **Arcanist**, but is
suitable for inclusion in other projects. In particular, some of the classes
provided in this library vastly improve the state of common operations in PHP,
like executing system commands.
libphutil is developed and maintained by
[[ http://www.phacility.com/ | Phacility ]]. Some of the code in this library
was originally developed at Facebook, and parts of it appear in the core
libraries for <http://www.facebook.com/>.
= Loading libphutil =
To include libphutil in another project, include the
`src/__phutil_library_init__.php` file:
require_once 'path/to/libphutil/src/__phutil_library_init__.php';
This loads global functions and registers an autoload function with
`spl_autoload_register()`, so you can also use classes.
= Major Components =
Some of the major components of libphutil are:
- **Core Utilities**: a collection of useful functions like @{function:ipull}
which simplify common data manipulation;
- **Filesystem**: classes like @{class:Filesystem} which provide a strict API
for filesystem access and throw exceptions on failure, making it easier to
write robust code which interacts with files;
- **Command Execution**: libphutil provides a powerful system command
primitive in @{class:ExecFuture} which makes it far easier to write
command-line scripts which execute system commands
(see @{article:Command Execution});
- **@{function:xsprintf}**: allows you to define `sprintf()`-style functions
which use custom conversions; and
- **Library System**: an introspectable, inventoried system for organizing
PHP code and managing dependencies, supported by static analysis.
= Extending and Contributing =
Information on extending and contributing to libphutil is available in the
Phabricator documentation:
- To get started as a contributor, see @{article@phabcontrib:Contributor
Introduction}.

View file

@ -0,0 +1,90 @@
@title Using Futures
@group future
Overview of how futures work in libphutil.
= Overview =
Futures (also called "Promises") are objects which represent the result of some
pending computation (like executing a command or making a request to another
server), but don't actually hold that result until the computation finishes.
They are used to simplify parallel programming, since you can pass the future
around as a representation for the real result while the real result is being
computed in the background. When the object is asked to return the actual
result, it blocks until the result is available.
libphutil provides a number of future-based APIs, as they strike a good balance
between ease of use and power for many of the domains where PHP is a reasonable
language choice.
Each type of future is used to do a different type of computation (for instance,
@{class:ExecFuture} executes system commands while @{class:HTTPFuture} executes
HTTP requests), but all of them behave in a basically similar way and can be
manipulated with the same top-level constructs.
= Basics =
You create a future by instantiating the relevant class and ask it to return the
result by calling `resolve()`:
$gzip_future = new ExecFuture('gzip %s', $some_file);
$gzip_future->start();
// The future is now executing in the background, and you can continue
// doing computation in this process by putting code here.
list($err, $stdout, $stderr) = $gzip_future->resolve();
When you call `resolve()`, the future blocks until the result is ready. You
can test if a future's result is ready by calling `isReady()`:
$is_ready = $gzip_future->isReady();
Being "ready" indicates that the future's computation has completed and it will
not need to block when you call `resolve()`.
Note that when you instantiate a future, it does not immediately initiate
computation. You must call `start()`, `isReady()` or `resolve()` to
activate it. If you simply call `resolve()` it will start, block until it is
complete, and then return the result, acting in a completely synchronous way.
See @{article:Command Execution} for more detailed documentation on how to
execute system commands with libphutil.
= Managing Multiple Futures =
Commonly, you may have many similar tasks you wish to parallelize: instead of
compressing one file, you want to compress several files. You can use the
@{class:FutureIterator} class to manage multiple futures.
$futures = array();
foreach ($files as $file) {
$futures[$file] = new ExecFuture("gzip %s", $file);
}
foreach (new FutureIterator($futures) as $file => $future) {
list($err, $stdout, $stderr) = $future->resolve();
if (!$err) {
echo "Compressed {$file}...\n";
} else {
echo "Failed to compress {$file}!\n";
}
}
@{class:FutureIterator} takes a list of futures and runs them in parallel,
**returning them in the order they resolve, NOT the original list order**. This
allows your program to begin any follow-up computation as quickly as possible:
if the slowest future in the list happens to be the first one, you can finish
processing all the other futures while waiting for it.
You can also limit how many futures you want to run at once. For instance, to
process no more than 4 files simultaneously:
foreach (id(new FutureIterator($futures))->limit(4) as $file => $future) {
// ...
}
Consult the @{class:FutureIterator} documentation for detailed information on
class capabilities.

View file

@ -0,0 +1,87 @@
{
"name": "libphutil",
"title": "libphutil Technical Documentation",
"short": "libphutil Tech Docs",
"preface": "Technical documentation for developers using libphutil.",
"root": "../../../",
"uri.source":
"https://secure.phabricator.com/diffusion/PHU/browse/master/%f$%l",
"rules": {
"(\\.diviner$)": "DivinerArticleAtomizer",
"(\\.php$)": "DivinerPHPAtomizer"
},
"exclude": [
"(^externals/)",
"(^resources/)",
"(^scripts/)",
"(^support/)"
],
"groups": {
"overview": {
"name": "libphutil Overview"
},
"aphront": {
"name": "Aphront",
"include": "(^src/aphront/)"
},
"auth": {
"name": "Authentication",
"include": "(^src/auth/)"
},
"conduit": {
"name": "Conduit",
"include": "(^src/conduit/)"
},
"console": {
"name": "Console",
"include": "(^src/console/)"
},
"daemon": {
"name": "Daemons",
"include": "(^src/daemon/)"
},
"error": {
"name": "Errors",
"include": "(^src/error/)"
},
"filesystem": {
"name": "Filesystem",
"include": "(^src/filesystem/)"
},
"future": {
"name": "Futures",
"include": "(^src/future/)"
},
"internationalization": {
"name": "Internationalization",
"include": "(^src/internationalization/)"
},
"lexer": {
"name": "Lexers",
"include": "(^src/lexer/)"
},
"library": {
"name": "libphutil Library System",
"include": "(^src/moduleutils/)"
},
"parser": {
"name": "Parsers",
"include": "(^src/parser/)"
},
"phage": {
"name": "Phage",
"include": "(^src/phage/)"
},
"remarkup": {
"name": "Remarkup",
"include": "(^src/markup/)"
},
"utf8": {
"name": "Handling Unicode and UTF-8"
},
"util": {
"name": "Core Utilities",
"include": "(^src/utils/)"
}
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* Exception that aggregates other exceptions into a single exception. For
* example, if you have several objects which can perform a task and just want
* at least one of them to succeed, you can do something like this:
*
* $exceptions = array();
* $success = false;
* foreach ($engines as $engine) {
* try {
* $engine->doSomething();
* $success = true;
* break;
* } catch (Exception $ex) {
* $exceptions[get_class($engine)] = $ex;
* }
* }
*
* if (!$success) {
* throw new PhutilAggregateException("All engines failed:", $exceptions);
* }
*
* @concrete-extensible
*/
class PhutilAggregateException extends Exception {
private $exceptions = array();
public function __construct($message, array $other_exceptions) {
// We don't call assert_instances_of($other_exceptions, 'Exception') to not
// throw another exception in this exception.
$this->exceptions = $other_exceptions;
$full_message = array();
$full_message[] = $message;
foreach ($other_exceptions as $key => $exception) {
$ex_message =
(is_string($key) ? $key.': ' : '').
get_class($exception).': '.
$exception->getMessage();
$ex_message = ' - '.str_replace("\n", "\n ", $ex_message);
$full_message[] = $ex_message;
}
parent::__construct(implode("\n", $full_message), count($other_exceptions));
}
public function getExceptions() {
return $this->exceptions;
}
}

View file

@ -0,0 +1,595 @@
<?php
/**
* Improve PHP error logs and optionally route errors, exceptions and debugging
* information to a central listener.
*
* This class takes over the PHP error and exception handlers when you call
* ##PhutilErrorHandler::initialize()## and forwards all debugging information
* to a listener you install with ##PhutilErrorHandler::setErrorListener()##.
*
* To use PhutilErrorHandler, which will enhance the messages printed to the
* PHP error log, just initialize it:
*
* PhutilErrorHandler::initialize();
*
* To additionally install a custom listener which can print error information
* to some other file or console, register a listener:
*
* PhutilErrorHandler::setErrorListener($some_callback);
*
* For information on writing an error listener, see
* @{function:phutil_error_listener_example}. Providing a listener is optional,
* you will benefit from improved error logs even without one.
*
* Phabricator uses this class to drive the DarkConsole "Error Log" plugin.
*
* @task config Configuring Error Dispatch
* @task exutil Exception Utilities
* @task trap Error Traps
* @task internal Internals
*/
final class PhutilErrorHandler extends Phobject {
private static $errorListener = null;
private static $initialized = false;
private static $traps = array();
const EXCEPTION = 'exception';
const ERROR = 'error';
const PHLOG = 'phlog';
const DEPRECATED = 'deprecated';
/* -( Configuring Error Dispatch )----------------------------------------- */
/**
* Registers this class as the PHP error and exception handler. This will
* overwrite any previous handlers!
*
* @return void
* @task config
*/
public static function initialize() {
self::$initialized = true;
set_error_handler(array(__CLASS__, 'handleError'));
set_exception_handler(array(__CLASS__, 'handleException'));
}
/**
* Provide an optional listener callback which will receive all errors,
* exceptions and debugging messages. It can then print them to a web console,
* for example.
*
* See @{function:phutil_error_listener_example} for details about the
* callback parameters and operation.
*
* @return void
* @task config
*/
public static function setErrorListener($listener) {
self::$errorListener = $listener;
}
/* -( Exception Utilities )------------------------------------------------ */
/**
* Gets the previous exception of a nested exception. Prior to PHP 5.3 you
* can use @{class:PhutilProxyException} to nest exceptions; after PHP 5.3
* all exceptions are nestable.
*
* @param Exception|Throwable Exception to unnest.
* @return Exception|Throwable|null Previous exception, if one exists.
* @task exutil
*/
public static function getPreviousException($ex) {
if (method_exists($ex, 'getPrevious')) {
return $ex->getPrevious();
}
if (method_exists($ex, 'getPreviousException')) {
return $ex->getPreviousException();
}
return null;
}
/**
* Find the most deeply nested exception from a possibly-nested exception.
*
* @param Exception|Throwable A possibly-nested exception.
* @return Exception|Throwable Deepest exception in the nest.
* @task exutil
*/
public static function getRootException($ex) {
$root = $ex;
while (self::getPreviousException($root)) {
$root = self::getPreviousException($root);
}
return $root;
}
/* -( Trapping Errors )---------------------------------------------------- */
/**
* Adds an error trap. Normally you should not invoke this directly;
* @{class:PhutilErrorTrap} registers itself on construction.
*
* @param PhutilErrorTrap Trap to add.
* @return void
* @task trap
*/
public static function addErrorTrap(PhutilErrorTrap $trap) {
$key = $trap->getTrapKey();
self::$traps[$key] = $trap;
}
/**
* Removes an error trap. Normally you should not invoke this directly;
* @{class:PhutilErrorTrap} deregisters itself on destruction.
*
* @param PhutilErrorTrap Trap to remove.
* @return void
* @task trap
*/
public static function removeErrorTrap(PhutilErrorTrap $trap) {
$key = $trap->getTrapKey();
unset(self::$traps[$key]);
}
/* -( Internals )---------------------------------------------------------- */
/**
* Determine if PhutilErrorHandler has been initialized.
*
* @return bool True if initialized.
* @task internal
*/
public static function hasInitialized() {
return self::$initialized;
}
/**
* Handles PHP errors and dispatches them forward. This is a callback for
* ##set_error_handler()##. You should not call this function directly; use
* @{function:phlog} to print debugging messages or ##trigger_error()## to
* trigger PHP errors.
*
* This handler converts E_RECOVERABLE_ERROR messages from violated typehints
* into @{class:InvalidArgumentException}s.
*
* This handler converts other E_RECOVERABLE_ERRORs into
* @{class:RuntimeException}s.
*
* This handler converts E_NOTICE messages from uses of undefined variables
* into @{class:RuntimeException}s.
*
* @param int Error code.
* @param string Error message.
* @param string File where the error occurred.
* @param int Line on which the error occurred.
* @param wild Error context information.
* @return void
* @task internal
*/
public static function handleError($num, $str, $file, $line, $ctx) {
foreach (self::$traps as $trap) {
$trap->addError($num, $str, $file, $line, $ctx);
}
if ((error_reporting() & $num) == 0) {
// Respect the use of "@" to silence warnings: if this error was
// emitted from a context where "@" was in effect, the
// value returned by error_reporting() will be 0. This is the
// recommended way to check for this, see set_error_handler() docs
// on php.net.
return false;
}
// Convert typehint failures into exceptions.
if (preg_match('/^Argument (\d+) passed to (\S+) must be/', $str)) {
throw new InvalidArgumentException($str);
}
// Convert other E_RECOVERABLE_ERRORs into generic runtime exceptions.
if ($num == E_RECOVERABLE_ERROR) {
throw new RuntimeException($str);
}
// Convert uses of undefined variables into exceptions.
if (preg_match('/^Undefined variable: /', $str)) {
throw new RuntimeException($str);
}
// Convert uses of undefined properties into exceptions.
if (preg_match('/^Undefined property: /', $str)) {
throw new RuntimeException($str);
}
// Convert undefined constants into exceptions. Usually this means there
// is a missing `$` and the program is horribly broken.
if (preg_match('/^Use of undefined constant /', $str)) {
throw new RuntimeException($str);
}
$trace = debug_backtrace();
array_shift($trace);
self::dispatchErrorMessage(
self::ERROR,
$str,
array(
'file' => $file,
'line' => $line,
'context' => $ctx,
'error_code' => $num,
'trace' => $trace,
));
}
/**
* Handles PHP exceptions and dispatches them forward. This is a callback for
* ##set_exception_handler()##. You should not call this function directly;
* to print exceptions, pass the exception object to @{function:phlog}.
*
* @param Exception|Throwable Uncaught exception object.
* @return void
* @task internal
*/
public static function handleException($ex) {
self::dispatchErrorMessage(
self::EXCEPTION,
$ex,
array(
'file' => $ex->getFile(),
'line' => $ex->getLine(),
'trace' => self::getExceptionTrace($ex),
'catch_trace' => debug_backtrace(),
));
// Normally, PHP exits with code 255 after an uncaught exception is thrown.
// However, if we install an exception handler (as we have here), it exits
// with code 0 instead. Script execution terminates after this function
// exits in either case, so exit explicitly with the correct exit code.
exit(255);
}
/**
* Output a stacktrace to the PHP error log.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @return void
* @task internal
*/
public static function outputStacktrace($trace) {
$lines = explode("\n", self::formatStacktrace($trace));
foreach ($lines as $line) {
error_log($line);
}
}
/**
* Format a stacktrace for output.
*
* @param trace A stacktrace, e.g. from debug_backtrace();
* @return string Human-readable trace.
* @task internal
*/
public static function formatStacktrace($trace) {
$result = array();
$libinfo = self::getLibraryVersions();
if ($libinfo) {
foreach ($libinfo as $key => $dict) {
$info = array();
foreach ($dict as $dkey => $dval) {
$info[] = $dkey.'='.$dval;
}
$libinfo[$key] = $key.'('.implode(', ', $info).')';
}
$result[] = implode(', ', $libinfo);
}
foreach ($trace as $key => $entry) {
$line = ' #'.$key.' ';
if (!empty($entry['xid'])) {
if ($entry['xid'] != 1) {
$line .= '<#'.$entry['xid'].'> ';
}
}
if (isset($entry['class'])) {
$line .= $entry['class'].'::';
}
$line .= idx($entry, 'function', '');
if (isset($entry['args'])) {
$args = array();
foreach ($entry['args'] as $arg) {
// NOTE: Print out object types, not values. Values sometimes contain
// sensitive information and are usually not particularly helpful
// for debugging.
$type = (gettype($arg) == 'object')
? get_class($arg)
: gettype($arg);
$args[] = $type;
}
$line .= '('.implode(', ', $args).')';
}
if (isset($entry['file'])) {
$file = self::adjustFilePath($entry['file']);
$line .= ' called at ['.$file.':'.$entry['line'].']';
}
$result[] = $line;
}
return implode("\n", $result);
}
/**
* All different types of error messages come here before they are
* dispatched to the listener; this method also prints them to the PHP error
* log.
*
* @param const Event type constant.
* @param wild Event value.
* @param dict Event metadata.
* @return void
* @task internal
*/
public static function dispatchErrorMessage($event, $value, $metadata) {
$timestamp = strftime('%Y-%m-%d %H:%M:%S');
switch ($event) {
case self::ERROR:
$default_message = sprintf(
'[%s] ERROR %d: %s at [%s:%d]',
$timestamp,
$metadata['error_code'],
$value,
$metadata['file'],
$metadata['line']);
$metadata['default_message'] = $default_message;
error_log($default_message);
self::outputStacktrace($metadata['trace']);
break;
case self::EXCEPTION:
$messages = array();
$current = $value;
do {
$messages[] = '('.get_class($current).') '.$current->getMessage();
} while ($current = self::getPreviousException($current));
$messages = implode(' {>} ', $messages);
if (strlen($messages) > 4096) {
$messages = substr($messages, 0, 4096).'...';
}
$default_message = sprintf(
'[%s] EXCEPTION: %s at [%s:%d]',
$timestamp,
$messages,
self::adjustFilePath(self::getRootException($value)->getFile()),
self::getRootException($value)->getLine());
$metadata['default_message'] = $default_message;
error_log($default_message);
self::outputStacktrace($metadata['trace']);
break;
case self::PHLOG:
$default_message = sprintf(
'[%s] PHLOG: %s at [%s:%d]',
$timestamp,
PhutilReadableSerializer::printShort($value),
$metadata['file'],
$metadata['line']);
$metadata['default_message'] = $default_message;
error_log($default_message);
break;
case self::DEPRECATED:
$default_message = sprintf(
'[%s] DEPRECATED: %s is deprecated; %s',
$timestamp,
$value,
$metadata['why']);
$metadata['default_message'] = $default_message;
error_log($default_message);
break;
default:
error_log(pht('Unknown event %s', $event));
break;
}
if (self::$errorListener) {
static $handling_error;
if ($handling_error) {
error_log(
'Error handler was reentered, some errors were not passed to the '.
'listener.');
return;
}
$handling_error = true;
call_user_func(self::$errorListener, $event, $value, $metadata);
$handling_error = false;
}
}
public static function adjustFilePath($path) {
// Compute known library locations so we can emit relative paths if the
// file resides inside a known library. This is a little cleaner to read,
// and limits the number of false positives we get about full path
// disclosure via HackerOne.
$bootloader = PhutilBootloader::getInstance();
$libraries = $bootloader->getAllLibraries();
$roots = array();
foreach ($libraries as $library) {
$root = $bootloader->getLibraryRoot($library);
// For these libraries, the effective root is one level up.
switch ($library) {
case 'arcanist':
case 'phabricator':
$root = dirname($root);
break;
}
if (!strncmp($root, $path, strlen($root))) {
return '<'.$library.'>'.substr($path, strlen($root));
}
}
return $path;
}
public static function getLibraryVersions() {
$libinfo = array();
$bootloader = PhutilBootloader::getInstance();
foreach ($bootloader->getAllLibraries() as $library) {
$root = phutil_get_library_root($library);
$try_paths = array(
$root,
dirname($root),
);
$libinfo[$library] = array();
$get_refs = array('master');
foreach ($try_paths as $try_path) {
// Try to read what the HEAD of the repository is pointed at. This is
// normally the name of a branch ("ref").
$try_file = $try_path.'/.git/HEAD';
if (@file_exists($try_file)) {
$head = @file_get_contents($try_file);
$matches = null;
if (preg_match('(^ref: refs/heads/(.*)$)', trim($head), $matches)) {
$libinfo[$library]['head'] = trim($matches[1]);
$get_refs[] = trim($matches[1]);
} else {
$libinfo[$library]['head'] = trim($head);
}
break;
}
}
// Try to read which commit relevant branch heads are at.
foreach (array_unique($get_refs) as $ref) {
foreach ($try_paths as $try_path) {
$try_file = $try_path.'/.git/refs/heads/'.$ref;
if (@file_exists($try_file)) {
$hash = @file_get_contents($try_file);
if ($hash) {
$libinfo[$library]['ref.'.$ref] = substr(trim($hash), 0, 12);
break;
}
}
}
}
// Look for extension files.
$custom = @scandir($root.'/extensions/');
if ($custom) {
$count = 0;
foreach ($custom as $custom_path) {
if (preg_match('/\.php$/', $custom_path)) {
$count++;
}
}
if ($count) {
$libinfo[$library]['custom'] = $count;
}
}
}
ksort($libinfo);
return $libinfo;
}
/**
* Get a full trace across all proxied and aggregated exceptions.
*
* This attempts to build a set of stack frames which completely represent
* all of the places an exception came from, even if it came from multiple
* origins and has been aggregated or proxied.
*
* @param Exception|Throwable Exception to retrieve a trace for.
* @return list<wild> List of stack frames.
*/
public static function getExceptionTrace($ex) {
$id = 1;
// Keep track of discovered exceptions which we need to build traces for.
$stack = array(
array($id, $ex),
);
$frames = array();
while ($info = array_shift($stack)) {
list($xid, $ex) = $info;
// We're going from top-level exception down in bredth-first order, but
// want to build a trace in approximately standard order (deepest part of
// the call stack to most shallow) so we need to reverse each list of
// frames and then reverse everything at the end.
$ex_frames = array_reverse($ex->getTrace());
$ex_frames = array_values($ex_frames);
$last_key = (count($ex_frames) - 1);
foreach ($ex_frames as $frame_key => $frame) {
$frame['xid'] = $xid;
// If this is a child/previous exception and we're on the deepest frame
// and missing file/line data, fill it in from the exception itself.
if ($xid > 1 && ($frame_key == $last_key)) {
if (empty($frame['file'])) {
$frame['file'] = $ex->getFile();
$frame['line'] = $ex->getLine();
}
}
// Since the exceptions are likely to share the most shallow frames,
// try to add those to the trace only once.
if (isset($frame['file']) && isset($frame['line'])) {
$signature = $frame['file'].':'.$frame['line'];
if (empty($frames[$signature])) {
$frames[$signature] = $frame;
}
} else {
$frames[] = $frame;
}
}
// If this is a proxy exception, add the proxied exception.
$prev = self::getPreviousException($ex);
if ($prev) {
$stack[] = array(++$id, $prev);
}
// If this is an aggregate exception, add the child exceptions.
if ($ex instanceof PhutilAggregateException) {
foreach ($ex->getExceptions() as $child) {
$stack[] = array(++$id, $child);
}
}
}
return array_values(array_reverse($frames));
}
}

View file

@ -0,0 +1,83 @@
<?php
/**
* Trap PHP errors while this object is alive, so they can be accessed and
* included in exceptions or other types of logging. For example, if you have
* code like this:
*
* $res = proc_open(...);
*
* There is no easy way to throw an informative exception if the `proc_open()`
* fails. In some cases you may be able to use `error_get_last()`, but this is
* unreliable (if `proc_open()` fails because `disable_functions` is set, it
* does not capture the error) and can not capture more than one error.
*
* You can trap errors while executing this code instead:
*
* $trap = new PhutilErrorTrap();
* $res = proc_open(...);
* $err = $trap->getErrorsAsString();
* $trap->destroy();
*
* if (!$res) {
* throw new Exception('proc_open() failed: '.$err);
* }
*
* IMPORTANT: You must explicitly destroy traps because they register
* themselves with @{class:PhutilErrorHandler}, and thus will not be destroyed
* when `unset()`.
*
* Some notes on traps:
*
* - Traps catch all errors, including those silenced by `@`.
* - Traps do not prevent errors from reaching other standard handlers. You
* can use `@` to keep errors out of the logs while still trapping them.
* - Traps capture all errors until they are explicitly destroyed. This means
* that you should not create long-lived traps, or they may consume
* unbounded amounts of memory to hold the error log.
*/
final class PhutilErrorTrap extends Phobject {
private $destroyed;
private $errors = array();
public function addError($num, $str, $file, $line, $ctx) {
$this->errors[] = array(
'num' => $num,
'str' => $str,
'file' => $file,
'line' => $line,
'ctx' => $ctx,
);
return $this;
}
public function getErrorsAsString() {
$out = array();
foreach ($this->errors as $error) {
$out[] = $error['str'];
}
return implode("\n", $out);
}
public function destroy() {
if (!$this->destroyed) {
PhutilErrorHandler::removeErrorTrap($this);
$this->errors = array();
$this->destroyed = true;
}
}
public function getTrapKey() {
return spl_object_hash($this);
}
public function __construct() {
PhutilErrorHandler::addErrorTrap($this);
}
public function __toString() {
return $this->getErrorsAsString();
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* An exception thrown when a method is called on a class which does not
* provide an implementation for the called method. This is sometimes the case
* when a base class expects subclasses to provide their own implementations,
* for example.
*/
final class PhutilMethodNotImplementedException extends Exception {
public function __construct($message = null) {
if ($message) {
parent::__construct($message);
} else {
$caller = idx(debug_backtrace(), 1);
if (isset($caller['object'])) {
$class = get_class($caller['object']);
} else {
$class = idx($caller, 'class');
}
$function = idx($caller, 'function');
if ($class) {
parent::__construct(
pht('Method %s in class %s is not implemented!', $function, $class));
} else {
parent::__construct(pht('Function %s is not implemented!', $function));
}
}
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* Opaque reference to a string (like a password) that won't put any sensitive
* data in stack traces, var_dump(), print_r(), error logs, etc. Usage:
*
* $envelope = new PhutilOpaqueEnvelope($password);
* do_stuff($envelope);
* // ...
* $password = $envelope->openEnvelope();
*
* Any time you're passing sensitive data into a stack, you should obscure it
* with an envelope to prevent it leaking if something goes wrong.
*
* The key for the envelope is stored elsewhere, in
* @{class:PhutilOpaqueEnvelopeKey}. This prevents it from appearing in
* any sort of logs related to the envelope, even if the logger is very
* aggressive.
*
* @task envelope Using Opaque Envelopes
* @task internal Internals
*/
final class PhutilOpaqueEnvelope extends Phobject {
private $value;
/* -( Using Opaque Envelopes )--------------------------------------------- */
/**
* @task envelope
*/
public function __construct($string) {
$this->value = $this->mask($string, PhutilOpaqueEnvelopeKey::getKey());
}
/**
* @task envelope
*/
public function openEnvelope() {
return $this->mask($this->value, PhutilOpaqueEnvelopeKey::getKey());
}
/**
* @task envelope
*/
public function __toString() {
return pht('<opaque envelope>');
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function mask($string, $noise) {
$result = '';
for ($ii = 0; $ii < strlen($string); $ii++) {
$s = $string[$ii];
$n = $noise[$ii % strlen($noise)];
$result .= chr(ord($s) ^ ord($n));
}
return $result;
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Holds the key for @{class:PhutilOpaqueEnvelope} in a logically distant
* location so it will never appear in stack traces, etc. You should never need
* to use this class directly. See @{class:PhutilOpaqueEnvelope} for
* information about opaque envelopes.
*
* @task internal Internals
*/
final class PhutilOpaqueEnvelopeKey extends Phobject {
private static $key;
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function __construct() {
// <private>
}
/**
* @task internal
*/
public static function getKey() {
if (self::$key === null) {
// NOTE: We're using a weak random source because cryptographic levels
// of security aren't terribly important here and it allows us to use
// envelopes on systems which don't have a strong random source. Notably,
// this lets us make it to the readability check for `/dev/urandom` in
// Phabricator on systems where we can't read it.
self::$key = '';
for ($ii = 0; $ii < 8; $ii++) {
self::$key .= md5(mt_rand(), $raw_output = true);
}
}
return self::$key;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* Prior to PHP 5.3, PHP does not support nested exceptions; this class provides
* limited support for nested exceptions. Use methods on
* @{class:PhutilErrorHandler} to unnest exceptions in a forward-compatible way.
*
* @concrete-extensible
*/
class PhutilProxyException extends Exception {
private $previousException;
public function __construct($message, $previous, $code = 0) {
$this->previousException = $previous;
// This may be an "Exception" or a "Throwable". The "__construct()" method
// for the Exception is documented as taking an Exception, not a Throwable.
// Although passing a Throwable appears to work in PHP 7.3, don't risk it.
$is_exception = ($previous instanceof Exception);
if (version_compare(PHP_VERSION, '5.3.0', '>=') && $is_exception) {
parent::__construct($message, $code, $previous);
} else {
parent::__construct($message, $code);
}
}
public function getPreviousException() {
// NOTE: This can not be named "getPrevious()" because that method is final
// after PHP 5.3. Similarly, the property can not be named "previous"
// because HPHP declares a property with the same name and "protected"
// visibility.
return $this->previousException;
}
}

View file

@ -0,0 +1,39 @@
<?php
final class PhutilErrorHandlerTestCase extends PhutilTestCase {
public function testProxyException() {
$a = new Exception('a');
$b = new PhutilProxyException('b', $a);
$c = new PhutilProxyException('c', $b);
$this->assertEqual($a, $b->getPrevious());
$this->assertEqual($a, PhutilErrorHandler::getRootException($b));
$this->assertEqual($a, PhutilErrorHandler::getPreviousException($b));
$this->assertEqual($a, PhutilErrorHandler::getRootException($c));
$this->assertEqual($b, PhutilErrorHandler::getPreviousException($c));
}
public function testSilenceHandler() {
// Errors should normally be logged.
$this->assertTrue(strlen($this->emitError()) > 0);
// The "@" operator should silence errors.
$this->assertTrue(@strlen($this->emitError()) === 0);
}
private function emitError() {
$temporary_log = new TempFile();
$old_log = ini_get('error_log');
ini_set('error_log', (string)$temporary_log);
trigger_error(pht('(A synthetic error emitted during a unit test.)'));
ini_set('error_log', $old_log);
return Filesystem::readFile($temporary_log);
}
}

View file

@ -0,0 +1,47 @@
<?php
final class PhutilOpaqueEnvelopeTestCase extends PhutilTestCase {
public function testOpaqueEnvelope() {
// NOTE: When run via "arc diff", this test's trace may include portions of
// the diff itself, and thus this source code. Since we look for the secret
// in traces later on, split it apart here so that invocation via
// "arc diff" doesn't create a false test failure.
$secret = 'hunter'.'2';
$envelope = new PhutilOpaqueEnvelope($secret);
$this->assertFalse(strpos(var_export($envelope, true), $secret));
$this->assertFalse(strpos(print_r($envelope, true), $secret));
ob_start();
var_dump($envelope);
$dump = ob_get_clean();
$this->assertFalse(strpos($dump, $secret));
try {
$this->throwTrace($envelope);
} catch (Exception $ex) {
$trace = $ex->getTrace();
$this->assertFalse(strpos(print_r($trace, true), $secret));
}
$backtrace = $this->getBacktrace($envelope);
$this->assertFalse(strpos(print_r($backtrace, true), $secret));
$this->assertEqual($secret, $envelope->openEnvelope());
}
private function throwTrace($v) {
throw new Exception('!');
}
private function getBacktrace($v) {
return debug_backtrace();
}
}

68
src/error/phlog.php Normal file
View file

@ -0,0 +1,68 @@
<?php
/**
* libphutil log function for development debugging. Takes any argument and
* forwards it to registered listeners. This is essentially a more powerful
* version of `error_log()`.
*
* @param wild Any value you want printed to the error log or other registered
* logs/consoles.
* @param ... Other values to be logged.
* @return wild Passed $value.
*/
function phlog($value/* , ... */) {
// Get the caller information.
$trace = debug_backtrace();
$metadata = array(
'file' => $trace[0]['file'],
'line' => $trace[0]['line'],
'trace' => $trace,
);
foreach (func_get_args() as $event) {
$data = $metadata;
if (($event instanceof Exception) || ($event instanceof Throwable)) {
$type = PhutilErrorHandler::EXCEPTION;
// If this is an exception, proxy it and generate a composite trace which
// shows both where the phlog() was called and where the exception was
// originally thrown from.
$proxy = new PhutilProxyException('', $event);
$trace = PhutilErrorHandler::getExceptionTrace($proxy);
$data['trace'] = $trace;
} else {
$type = PhutilErrorHandler::PHLOG;
}
PhutilErrorHandler::dispatchErrorMessage($type, $event, $data);
}
return $value;
}
/**
* Example @{class:PhutilErrorHandler} error listener callback. When you call
* `PhutilErrorHandler::setErrorListener()`, you must pass a callback function
* with the same signature as this one.
*
* NOTE: @{class:PhutilErrorHandler} handles writing messages to the error
* log, so you only need to provide a listener if you have some other console
* (like Phabricator's DarkConsole) which you //also// want to send errors to.
*
* NOTE: You will receive errors which were silenced with the `@` operator. If
* you don't want to display these, test for `@` being in effect by checking if
* `error_reporting() === 0` before displaying the error.
*
* @param const A PhutilErrorHandler constant, like PhutilErrorHandler::ERROR,
* which indicates the event type (e.g. error, exception,
* user message).
* @param wild The event value, like the Exception object for an exception
* event, an error string for an error event, or some user object
* for user messages.
* @param dict A dictionary of metadata about the event. The keys 'file',
* 'line' and 'trace' are always available. Other keys may be
* present, depending on the event type.
* @return void
*/
function phutil_error_listener_example($event, $value, array $metadata) {
throw new Exception(pht('This is just an example function!'));
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @concrete-extensible
*/
class PhutilEvent extends Phobject {
private $type;
private $data;
private $stop = false;
public function __construct($type, array $data = array()) {
$this->type = $type;
$this->data = $data;
}
public function getType() {
return $this->type;
}
public function getValue($key, $default = null) {
return idx($this->data, $key, $default);
}
public function setValue($key, $value) {
$this->data[$key] = $value;
return $this;
}
public function stop() {
$this->stop = true;
return $this;
}
public function isStopped() {
return $this->stop;
}
}

View file

@ -0,0 +1,75 @@
<?php
final class PhutilEventEngine extends Phobject {
private static $instance;
private $listeners = array();
private function __construct() {
// <empty>
}
public static function getInstance() {
if (!self::$instance) {
self::$instance = new PhutilEventEngine();
}
return self::$instance;
}
public function addListener(PhutilEventListener $listener, $type) {
$this->listeners[$type][] = $listener;
return $this;
}
/**
* Get all the objects currently listening to any event.
*/
public function getAllListeners() {
$listeners = array_mergev($this->listeners);
$listeners = mpull($listeners, null, 'getListenerID');
return $listeners;
}
public static function dispatchEvent(PhutilEvent $event) {
$instance = self::getInstance();
$listeners = idx($instance->listeners, $event->getType(), array());
$global_listeners = idx(
$instance->listeners,
PhutilEventType::TYPE_ALL,
array());
// Merge and deduplicate listeners (we want to send the event to each
// listener only once, even if it satisfies multiple criteria for the
// event).
$listeners = array_merge($listeners, $global_listeners);
$listeners = mpull($listeners, null, 'getListenerID');
$profiler = PhutilServiceProfiler::getInstance();
$profiler_id = $profiler->beginServiceCall(
array(
'type' => 'event',
'kind' => $event->getType(),
'count' => count($listeners),
));
$caught = null;
try {
foreach ($listeners as $listener) {
if ($event->isStopped()) {
// Do this first so if someone tries to dispatch a stopped event it
// doesn't go anywhere. Silly but less surprising.
break;
}
$listener->handleEvent($event);
}
} catch (Exception $ex) {
$profiler->endServiceCall($profiler_id, array());
throw $ex;
}
$profiler->endServiceCall($profiler_id, array());
}
}

View file

@ -0,0 +1,37 @@
<?php
abstract class PhutilEventListener extends Phobject {
private $listenerID;
private static $nextListenerID = 1;
final public function __construct() {
// <empty>
}
abstract public function register();
abstract public function handleEvent(PhutilEvent $event);
final public function listen($type) {
$engine = PhutilEventEngine::getInstance();
$engine->addListener($this, $type);
}
/**
* Return a scalar ID unique to this listener. This is used to deduplicate
* listeners which match events on multiple rules, so they are invoked only
* once.
*
* @return int A scalar unique to this object instance.
*/
final public function getListenerID() {
if (!$this->listenerID) {
$this->listenerID = self::$nextListenerID;
self::$nextListenerID++;
}
return $this->listenerID;
}
}

View file

@ -0,0 +1,3 @@
<?php
abstract class PhutilEventConstants extends Phobject {}

View file

@ -0,0 +1,10 @@
<?php
/**
* @concrete-extensible
*/
class PhutilEventType extends PhutilEventConstants {
const TYPE_ALL = '*';
}

View file

@ -0,0 +1,30 @@
<?php
final class PhutilInvalidStateException extends Exception {
private $callee;
private $function;
public function __construct($function, $callee = null) {
if ($callee === null) {
$callee = idx(debug_backtrace(), 1);
$callee = idx($callee, 'function');
}
$this->callee = $callee;
$this->function = $function;
parent::__construct(
pht(
'Call %s before calling %s!',
$this->function.'()',
$this->callee.'()'));
}
public function getCallee() {
return $this->callee;
}
public function getFunction() {
return $this->function;
}
}

View file

@ -0,0 +1,17 @@
<?php
final class PhutilInvalidStateExceptionTestCase extends PhutilTestCase {
public function testException() {
try {
throw new PhutilInvalidStateException('someMethod');
} catch (PhutilInvalidStateException $ex) {
$this->assertEqual(
__FUNCTION__,
$ex->getCallee());
$this->assertEqual(
'someMethod',
$ex->getFunction());
}
}
}

View file

@ -0,0 +1,365 @@
<?php
/**
* Find files on disk matching criteria, like the 'find' system utility. Use of
* this class is straightforward:
*
* // Find PHP files in /tmp
* $files = id(new FileFinder('/tmp'))
* ->withType('f')
* ->withSuffix('php')
* ->find();
*
* @task create Creating a File Query
* @task config Configuring File Queries
* @task exec Executing the File Query
* @task internal Internal
*/
final class FileFinder extends Phobject {
private $root;
private $exclude = array();
private $paths = array();
private $name = array();
private $suffix = array();
private $nameGlobs = array();
private $type;
private $generateChecksums = false;
private $followSymlinks;
private $forceMode;
/**
* Create a new FileFinder.
*
* @param string Root directory to find files beneath.
* @return this
* @task create
*/
public function __construct($root) {
$this->root = rtrim($root, '/');
}
/**
* @task config
*/
public function excludePath($path) {
$this->exclude[] = $path;
return $this;
}
/**
* @task config
*/
public function withName($name) {
$this->name[] = $name;
return $this;
}
/**
* @task config
*/
public function withSuffix($suffix) {
$this->suffix[] = $suffix;
return $this;
}
/**
* @task config
*/
public function withPath($path) {
$this->paths[] = $path;
return $this;
}
/**
* @task config
*/
public function withType($type) {
$this->type = $type;
return $this;
}
/**
* @task config
*/
public function withFollowSymlinks($follow) {
$this->followSymlinks = $follow;
return $this;
}
/**
* @task config
*/
public function setGenerateChecksums($generate) {
$this->generateChecksums = $generate;
return $this;
}
public function getGenerateChecksums() {
return $this->generateChecksums;
}
public function withNameGlob($pattern) {
$this->nameGlobs[] = $pattern;
return $this;
}
/**
* @task config
* @param string Either "php", "shell", or the empty string.
*/
public function setForceMode($mode) {
$this->forceMode = $mode;
return $this;
}
/**
* @task internal
*/
public function validateFile($file) {
if ($this->name) {
$matches = false;
foreach ($this->name as $curr_name) {
if (basename($file) === $curr_name) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
}
if ($this->nameGlobs) {
$name = basename($file);
$matches = false;
foreach ($this->nameGlobs as $glob) {
$glob = addcslashes($glob, '\\');
if (fnmatch($glob, $name)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
}
if ($this->suffix) {
$matches = false;
foreach ($this->suffix as $suffix) {
$suffix = addcslashes($suffix, '\\?*');
$suffix = '*.'.$suffix;
if (fnmatch($suffix, $file)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
}
if ($this->paths) {
$matches = false;
foreach ($this->paths as $path) {
if (fnmatch($path, $this->root.'/'.$file)) {
$matches = true;
break;
}
}
if (!$matches) {
return false;
}
}
$fullpath = $this->root.'/'.ltrim($file, '/');
if (($this->type == 'f' && is_dir($fullpath))
|| ($this->type == 'd' && !is_dir($fullpath))) {
return false;
}
return true;
}
/**
* @task internal
*/
private function getFiles($dir) {
$found = Filesystem::listDirectory($this->root.'/'.$dir, true);
$files = array();
if (strlen($dir) > 0) {
$dir = rtrim($dir, '/').'/';
}
foreach ($found as $filename) {
// Only exclude files whose names match relative to the root.
if ($dir == '') {
$matches = true;
foreach ($this->exclude as $exclude_path) {
if (fnmatch(ltrim($exclude_path, './'), $dir.$filename)) {
$matches = false;
break;
}
}
if (!$matches) {
continue;
}
}
if ($this->validateFile($dir.$filename)) {
$files[] = $dir.$filename;
}
if (is_dir($this->root.'/'.$dir.$filename)) {
foreach ($this->getFiles($dir.$filename) as $file) {
$files[] = $file;
}
}
}
return $files;
}
/**
* @task exec
*/
public function find() {
$files = array();
if (!is_dir($this->root) || !is_readable($this->root)) {
throw new Exception(
pht(
"Invalid %s root directory specified ('%s'). Root directory ".
"must be a directory, be readable, and be specified with an ".
"absolute path.",
__CLASS__,
$this->root));
}
if ($this->forceMode == 'shell') {
$php_mode = false;
} else if ($this->forceMode == 'php') {
$php_mode = true;
} else {
$php_mode = (phutil_is_windows() || !Filesystem::binaryExists('find'));
}
if ($php_mode) {
$files = $this->getFiles('');
} else {
$args = array();
$command = array();
$command[] = 'find';
if ($this->followSymlinks) {
$command[] = '-L';
}
$command[] = '.';
if ($this->exclude) {
$command[] = $this->generateList('path', $this->exclude).' -prune';
$command[] = '-o';
}
if ($this->type) {
$command[] = '-type %s';
$args[] = $this->type;
}
if ($this->name) {
$command[] = $this->generateList('name', $this->name, 'name');
}
if ($this->suffix) {
$command[] = $this->generateList('name', $this->suffix, 'suffix');
}
if ($this->paths) {
$command[] = $this->generateList('path', $this->paths);
}
if ($this->nameGlobs) {
$command[] = $this->generateList('name', $this->nameGlobs);
}
$command[] = '-print0';
array_unshift($args, implode(' ', $command));
list($stdout) = newv('ExecFuture', $args)
->setCWD($this->root)
->resolvex();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return array();
}
$files = explode("\0", $stdout);
// On OSX/BSD, find prepends a './' to each file.
foreach ($files as $key => $file) {
// When matching directories, we can get "." back in the result set,
// but this isn't an interesting result.
if ($file == '.') {
unset($files[$key]);
continue;
}
if (substr($files[$key], 0, 2) == './') {
$files[$key] = substr($files[$key], 2);
}
}
}
if (!$this->generateChecksums) {
return $files;
} else {
$map = array();
foreach ($files as $line) {
$fullpath = $this->root.'/'.ltrim($line, '/');
if (is_dir($fullpath)) {
$map[$line] = null;
} else {
$map[$line] = md5_file($fullpath);
}
}
return $map;
}
}
/**
* @task internal
*/
private function generateList(
$flag,
array $items,
$mode = 'glob') {
foreach ($items as $key => $item) {
// If the mode is not "glob" mode, we're going to escape glob characters
// in the pattern. Otherwise, we escape only backslashes.
if ($mode === 'glob') {
$item = addcslashes($item, '\\');
} else {
$item = addcslashes($item, '\\*?');
}
if ($mode === 'suffix') {
$item = '*.'.$item;
}
$item = (string)csprintf('%s %s', '-'.$flag, $item);
$items[$key] = $item;
}
$items = implode(' -o ', $items);
return '"(" '.$items.' ")"';
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* A list of files, primarily useful for parsing command line arguments. This
* class makes it easier to deal with user-specified lists of files and
* directories used by command line tools.
*
* $list = new FileList(array_slice($argv, 1));
* foreach ($some_files as $file) {
* if ($list->contains($file)) {
* do_something_to_this($file);
* }
* }
*
* This sort of construction will allow the user to type "src" in order
* to indicate 'all relevant files underneath "src/"'.
*
* @task create Creating a File List
* @task test Testing File Lists
*/
final class FileList extends Phobject {
private $files = array();
private $dirs = array();
/**
* Build a new FileList from an array of paths, e.g. from $argv.
*
* @param list List of relative or absolute file paths.
* @return this
* @task create
*/
public function __construct($paths) {
foreach ($paths as $path) {
$path = Filesystem::resolvePath($path);
if (is_dir($path)) {
$path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
$this->dirs[$path] = true;
}
$this->files[] = $path;
}
}
/**
* Determine if a path is one of the paths in the list. Note that an empty
* file list is considered to contain every file.
*
* @param string Relative or absolute system file path.
* @param bool If true, consider the path to be contained in the list if
* the list contains a parent directory. If false, require
* that the path be part of the list explicitly.
* @return bool If true, the file is in the list.
* @task test
*/
public function contains($path, $allow_parent_directory = true) {
if ($this->isEmpty()) {
return true;
}
$path = Filesystem::resolvePath($path);
if (is_dir($path)) {
$path .= DIRECTORY_SEPARATOR;
}
foreach ($this->files as $file) {
if ($file == $path) {
return true;
}
if ($allow_parent_directory) {
$len = strlen($file);
if (isset($this->dirs[$file]) && !strncmp($file, $path, $len)) {
return true;
}
}
}
return false;
}
/**
* Check if the file list is empty -- that is, it contains no files.
*
* @return bool If true, the list is empty.
* @task test
*/
public function isEmpty() {
return !$this->files;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
<?php
/**
* Exception thrown by Filesystem to indicate an error accessing the file
* system.
*/
final class FilesystemException extends Exception {
protected $path;
/**
* Create a new FilesystemException, providing a path and a message.
*
* @param string Path that caused the failure.
* @param string Description of the failure.
*/
public function __construct($path, $message) {
$this->path = $path;
parent::__construct($message);
}
/**
* Retrieve the path associated with the exception. Generally, this is
* something like a path that couldn't be read or written, or a path that
* was expected to exist but didn't.
*
* @return string Path associated with the exception.
*/
public function getPath() {
return $this->path;
}
}

View file

@ -0,0 +1,246 @@
<?php
/**
* Object that writes to a logfile when it is destroyed. This allows you to add
* more data to the log as execution unfolds, while still ensuring a write in
* normal circumstances (see below for discussion of cases where writes may not
* occur).
*
* Create the object with a logfile and format:
*
* $log = new PhutilDeferredLog('/path/to/access.log', "[%T]\t%u");
*
* Update the log with information as it becomes available:
*
* $log->setData(
* array(
* 'T' => date('c'),
* 'u' => $username,
* ));
*
* The log will be appended when the object's destructor is called, or when you
* invoke @{method:write}. Note that programs can exit without invoking object
* destructors (e.g., in the case of an unhandled exception, memory exhaustion,
* or SIGKILL) so writes are not guaranteed. You can call @{method:write} to
* force an explicit write to disk before the destructor is called.
*
* Log variables will be written with bytes 0x00-0x1F, 0x7F-0xFF, and backslash
* escaped using C-style escaping. Since this range includes tab, you can use
* tabs as field separators to ensure the file format is easily parsable. In
* PHP, you can decode this encoding with `stripcslashes`.
*
* If a variable is included in the log format but a value is never provided
* with @{method:setData}, it will be written as "-".
*
* @task log Logging
* @task write Writing the Log
* @task internal Internals
*/
final class PhutilDeferredLog extends Phobject {
private $file;
private $format;
private $data;
private $didWrite;
private $failQuietly;
/* -( Logging )------------------------------------------------------------ */
/**
* Create a new log entry, which will be written later. The format string
* should use "%x"-style placeholders to represent data which will be added
* later:
*
* $log = new PhutilDeferredLog('/some/file.log', '[%T] %u');
*
* @param string|null The file the entry should be written to, or null to
* create a log object which does not write anywhere.
* @param string The log entry format.
* @task log
*/
public function __construct($file, $format) {
$this->file = $file;
$this->format = $format;
$this->data = array();
$this->didWrite = false;
}
/**
* Add data to the log. Provide a map of variables to replace in the format
* string. For example, if you use a format string like:
*
* "[%T]\t%u"
*
* ...you might add data like this:
*
* $log->setData(
* array(
* 'T' => date('c'),
* 'u' => $username,
* ));
*
* When the log is written, the "%T" and "%u" variables will be replaced with
* the values you provide.
*
* @param dict Map of variables to values.
* @return this
* @task log
*/
public function setData(array $map) {
$this->data = $map + $this->data;
return $this;
}
/**
* Get existing log data.
*
* @param string Log data key.
* @param wild Default to return if data does not exist.
* @return wild Data, or default if data does not exist.
* @task log
*/
public function getData($key, $default = null) {
return idx($this->data, $key, $default);
}
/**
* Set the path where the log will be written. You can pass `null` to prevent
* the log from writing.
*
* NOTE: You can not change the file after the log writes.
*
* @param string|null File where the entry should be written to, or null to
* prevent writes.
* @return this
* @task log
*/
public function setFile($file) {
if ($this->didWrite) {
throw new Exception(
pht('You can not change the logfile after a write has occurred!'));
}
$this->file = $file;
return $this;
}
public function getFile() {
return $this->file;
}
/**
* Set quiet (logged) failure, instead of the default loud (exception)
* failure. Throwing exceptions from destructors which exit at the end of a
* request can result in difficult-to-debug behavior.
*/
public function setFailQuietly($fail_quietly) {
$this->failQuietly = $fail_quietly;
return $this;
}
/* -( Writing the Log )---------------------------------------------------- */
/**
* When the log object is destroyed, it writes if it hasn't written yet.
* @task write
*/
public function __destruct() {
$this->write();
}
/**
* Write the log explicitly, if it hasn't been written yet. Normally you do
* not need to call this method; it will be called when the log object is
* destroyed. However, you can explicitly force the write earlier by calling
* this method.
*
* A log object will never write more than once, so it is safe to call this
* method even if the object's destructor later runs.
*
* @return this
* @task write
*/
public function write() {
if ($this->didWrite) {
return $this;
}
// Even if we aren't going to write, format the line to catch any errors
// and invoke possible __toString() calls.
$line = $this->format();
try {
if ($this->file !== null) {
$dir = dirname($this->file);
if (!Filesystem::pathExists($dir)) {
Filesystem::createDirectory($dir, 0755, true);
}
$ok = @file_put_contents(
$this->file,
$line,
FILE_APPEND | LOCK_EX);
if ($ok === false) {
throw new Exception(
pht(
'Unable to write to logfile "%s"!',
$this->file));
}
}
} catch (Exception $ex) {
if ($this->failQuietly) {
phlog($ex);
} else {
throw $ex;
}
}
$this->didWrite = true;
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Format the log string, replacing "%x" variables with values.
*
* @return string Finalized, log string for writing to disk.
* @task internals
*/
private function format() {
// Always convert '%%' to literal '%'.
$map = array('%' => '%') + $this->data;
$result = '';
$saw_percent = false;
foreach (phutil_utf8v($this->format) as $c) {
if ($saw_percent) {
$saw_percent = false;
if (array_key_exists($c, $map)) {
$result .= addcslashes($map[$c], "\0..\37\\\177..\377");
} else {
$result .= '-';
}
} else if ($c == '%') {
$saw_percent = true;
} else {
$result .= $c;
}
}
return rtrim($result)."\n";
}
}

View file

@ -0,0 +1,50 @@
<?php
final class PhutilDirectoryFixture extends Phobject {
protected $path;
public static function newFromArchive($archive) {
$obj = self::newEmptyFixture();
execx(
'tar -C %s -xzvvf %s',
$obj->getPath(),
Filesystem::resolvePath($archive));
return $obj;
}
public static function newEmptyFixture() {
$obj = new PhutilDirectoryFixture();
$obj->path = Filesystem::createTemporaryDirectory();
return $obj;
}
private function __construct() {
// <restricted>
}
public function __destruct() {
Filesystem::remove($this->path);
}
public function getPath($to_file = null) {
return $this->path.'/'.ltrim($to_file, '/');
}
public function saveToArchive($path) {
$tmp = new TempFile();
execx(
'tar -C %s -czvvf %s .',
$this->getPath(),
$tmp);
$ok = rename($tmp, Filesystem::resolvePath($path));
if (!$ok) {
throw new FilesystemException($path, pht('Failed to overwrite file.'));
}
return $this;
}
}

View file

@ -0,0 +1,119 @@
<?php
/**
* Wrapper around `flock()` for advisory filesystem locking. Usage is
* straightforward:
*
* $path = '/path/to/lock.file';
* $lock = PhutilFileLock::newForPath($path);
* $lock->lock();
*
* do_contentious_things();
*
* $lock->unlock();
*
* For more information on locks, see @{class:PhutilLock}.
*
* @task construct Constructing Locks
* @task impl Implementation
*/
final class PhutilFileLock extends PhutilLock {
private $lockfile;
private $handle;
/* -( Constructing Locks )------------------------------------------------- */
/**
* Create a new lock on a lockfile. The file need not exist yet.
*
* @param string The lockfile to use.
* @return PhutilFileLock New lock object.
*
* @task construct
*/
public static function newForPath($lockfile) {
$lockfile = Filesystem::resolvePath($lockfile);
$name = 'file:'.$lockfile;
$lock = self::getLock($name);
if (!$lock) {
$lock = new PhutilFileLock($name);
$lock->lockfile = $lockfile;
self::registerLock($lock);
}
return $lock;
}
/* -( Locking )------------------------------------------------------------ */
/**
* Acquire the lock. If lock acquisition fails because the lock is held by
* another process, throws @{class:PhutilLockException}. Other exceptions
* indicate that lock acquisition has failed for reasons unrelated to locking.
*
* If the lock is already held, this method throws. You can test the lock
* status with @{method:isLocked}.
*
* @param float Seconds to block waiting for the lock.
* @return void
*
* @task lock
*/
protected function doLock($wait) {
$path = $this->lockfile;
$handle = @fopen($path, 'a+');
if (!$handle) {
throw new FilesystemException(
$path,
pht("Unable to open lock '%s' for writing!", $path));
}
$start_time = microtime(true);
do {
$would_block = null;
$ok = flock($handle, LOCK_EX | LOCK_NB, $would_block);
if ($ok) {
break;
} else {
usleep(10000);
}
} while ($wait && $wait > (microtime(true) - $start_time));
if (!$ok) {
fclose($handle);
throw new PhutilLockException($this->getName());
}
$this->handle = $handle;
}
/**
* Release the lock. Throws an exception on failure, e.g. if the lock is not
* currently held.
*
* @return void
*
* @task lock
*/
protected function doUnlock() {
$ok = flock($this->handle, LOCK_UN | LOCK_NB);
if (!$ok) {
throw new Exception(pht('Unable to unlock file!'));
}
$ok = fclose($this->handle);
if (!$ok) {
throw new Exception(pht('Unable to close file!'));
}
$this->handle = null;
}
}

View file

@ -0,0 +1,112 @@
<?php
/**
* Data structure for representing filesystem directory trees.
*/
final class PhutilFileTree extends Phobject {
private $name;
private $fullPath;
private $data;
private $depth = 0;
private $parentNode;
private $children = array();
public function addPath($path, $data) {
$parts = $this->splitPath($path);
$parts = array_reverse($parts);
$this->insertPath($parts, $data);
return $this;
}
public function destroy() {
$this->parentNode = null;
foreach ($this->children as $child) {
$child->destroy();
}
$this->children = array();
return $this;
}
/**
* Get the next node, iterating in depth-first order.
*/
public function getNextNode() {
if ($this->children) {
return head($this->children);
}
$cursor = $this;
while ($cursor) {
if ($cursor->getNextSibling()) {
return $cursor->getNextSibling();
}
$cursor = $cursor->parentNode;
}
return null;
}
public function getName() {
return $this->name;
}
public function getFullPath() {
return $this->fullPath;
}
public function getDepth() {
return $this->depth;
}
public function getData() {
return $this->data;
}
protected function insertPath(array $parts, $data) {
$part = array_pop($parts);
if ($part === null) {
if ($this->data) {
$full_path = $this->getFullPath();
throw new Exception(
pht("Duplicate insertion for path '%s'.", $full_path));
}
$this->data = $data;
return;
}
if (empty($this->children[$part])) {
$node = new PhutilFileTree();
$node->parentNode = $this;
$node->depth = $this->depth + 1;
$node->name = $part;
$node->fullPath = $this->parentNode ? ($this->fullPath.'/'.$part) : $part;
$this->children[$part] = $node;
}
$this->children[$part]->insertPath($parts, $data);
}
protected function splitPath($path) {
$path = trim($path, '/');
$parts = preg_split('@/+@', $path);
return $parts;
}
protected function getNextSibling() {
if (!$this->parentNode) {
return null;
}
$found = false;
foreach ($this->parentNode->children as $node) {
if ($found) {
return $node;
}
if ($this->name === $node->name) {
$found = true;
}
}
return null;
}
}

View file

@ -0,0 +1,235 @@
<?php
/**
* Base class for locks, like file locks.
*
* libphutil provides a concrete lock in @{class:PhutilFileLock}.
*
* $lock->lock();
* do_contentious_things();
* $lock->unlock();
*
* If the lock can't be acquired because it is already held,
* @{class:PhutilLockException} is thrown. Other exceptions indicate
* permanent failure unrelated to locking.
*
* When extending this class, you should call @{method:getLock} to look up
* an existing lock object, and @{method:registerLock} when objects are
* constructed to register for automatic unlock on shutdown.
*
* @task impl Lock Implementation
* @task registry Lock Registry
* @task construct Constructing Locks
* @task status Determining Lock Status
* @task lock Locking
* @task internal Internals
*/
abstract class PhutilLock extends Phobject {
private static $registeredShutdownFunction = false;
private static $locks = array();
private $locked = false;
private $profilerID;
private $name;
/* -( Constructing Locks )------------------------------------------------- */
/**
* Build a new lock, given a lock name. The name should be globally unique
* across all locks.
*
* @param string Globally unique lock name.
* @task construct
*/
protected function __construct($name) {
$this->name = $name;
}
/* -( Lock Implementation )------------------------------------------------ */
/**
* Acquires the lock, or throws @{class:PhutilLockException} if it fails.
*
* @param float Seconds to block waiting for the lock.
* @return void
* @task impl
*/
abstract protected function doLock($wait);
/**
* Releases the lock.
*
* @return void
* @task impl
*/
abstract protected function doUnlock();
/* -( Lock Registry )------------------------------------------------------ */
/**
* Returns a globally unique name for this lock.
*
* @return string Globally unique lock name, across all locks.
* @task registry
*/
final public function getName() {
return $this->name;
}
/**
* Get a named lock, if it has been registered.
*
* @param string Lock name.
* @task registry
*/
protected static function getLock($name) {
return idx(self::$locks, $name);
}
/**
* Register a lock for cleanup when the process exits.
*
* @param PhutilLock Lock to register.
* @task registry
*/
protected static function registerLock(PhutilLock $lock) {
if (!self::$registeredShutdownFunction) {
register_shutdown_function(array(__CLASS__, 'unlockAll'));
self::$registeredShutdownFunction = true;
}
$name = $lock->getName();
if (self::getLock($name)) {
throw new Exception(
pht("Lock '%s' is already registered!", $name));
}
self::$locks[$name] = $lock;
}
/* -( Determining Lock Status )-------------------------------------------- */
/**
* Determine if the lock is currently held.
*
* @return bool True if the lock is held.
*
* @task status
*/
final public function isLocked() {
return $this->locked;
}
/* -( Locking )------------------------------------------------------------ */
/**
* Acquire the lock. If lock acquisition fails because the lock is held by
* another process, throws @{class:PhutilLockException}. Other exceptions
* indicate that lock acquisition has failed for reasons unrelated to locking.
*
* If the lock is already held by this process, this method throws. You can
* test the lock status with @{method:isLocked}.
*
* @param float Seconds to block waiting for the lock. By default, do not
* block.
* @return this
*
* @task lock
*/
final public function lock($wait = 0) {
if ($this->locked) {
$name = $this->getName();
throw new Exception(
pht("Lock '%s' has already been locked by this process.", $name));
}
$profiler = PhutilServiceProfiler::getInstance();
$profiler_id = $profiler->beginServiceCall(
array(
'type' => 'lock',
'name' => $this->getName(),
));
try {
$this->doLock((float)$wait);
} catch (Exception $ex) {
$profiler->endServiceCall(
$profiler_id,
array(
'lock' => false,
));
throw $ex;
}
$this->profilerID = $profiler_id;
$this->locked = true;
return $this;
}
/**
* Release the lock. Throws an exception on failure, e.g. if the lock is not
* currently held.
*
* @return this
*
* @task lock
*/
final public function unlock() {
if (!$this->locked) {
$name = $this->getName();
throw new Exception(
pht("Lock '%s is not locked by this process!", $name));
}
$this->doUnlock();
$profiler = PhutilServiceProfiler::getInstance();
$profiler->endServiceCall(
$this->profilerID,
array(
'lock' => true,
));
$this->profilerID = null;
$this->locked = false;
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* On shutdown, we release all the locks. You should not call this method
* directly. Use @{method:unlock} to release individual locks.
*
* @return void
*
* @task internal
*/
public static function unlockAll() {
foreach (self::$locks as $key => $lock) {
if ($lock->locked) {
$lock->unlock();
}
}
}
}

View file

@ -0,0 +1,16 @@
<?php
final class PhutilLockException extends Exception {
private $hint;
public function setHint($hint) {
$this->hint = $hint;
return $this;
}
public function getHint() {
return $this->hint;
}
}

View file

@ -0,0 +1,125 @@
<?php
final class PhutilProcessQuery
extends Phobject {
private $isOverseer;
private $instances;
public function withIsOverseer($is_overseer) {
$this->isOverseer = $is_overseer;
return $this;
}
public function withInstances(array $instances) {
$this->instances = $instances;
return $this;
}
public function execute() {
if (phutil_is_windows()) {
throw new Exception(
pht(
'Querying system processes is not currently supported on '.
'Windows.'));
}
// TODO: See T12827. This formulation likely does not work properly on
// Solaris.
list($processes) = execx('ps -o pid,command -a -x -w -w -w');
$processes = phutil_split_lines($processes, false);
$refs = array();
foreach ($processes as $process) {
$parts = preg_split('/\s+/', trim($process), 2);
list($pid, $command) = $parts;
$ref = id(new PhutilProcessRef())
->setPID((int)$pid);
$argv = $this->getArgv($pid, $command);
$ref->setArgv($argv);
// If this is an overseer and the command has a "-l" ("Label") argument,
// the argument contains the "PHABRICATOR_INSTANCE" value for the daemon.
// Parse it out and annotate the process.
$instance = null;
if ($ref->getIsOverseer()) {
$matches = null;
if (preg_match('/-l (\S+)/', $command, $matches)) {
$instance = $matches[1];
}
}
$ref->setInstance($instance);
$refs[] = $ref;
}
if ($this->isOverseer !== null) {
foreach ($refs as $key => $ref) {
if ($ref->getIsOverseer() !== $this->isOverseer) {
unset($refs[$key]);
}
}
}
if ($this->instances) {
$instances_map = array_fuse($this->instances);
foreach ($refs as $key => $ref) {
if (!isset($instances_map[$ref->getInstance()])) {
unset($refs[$key]);
}
}
}
return array_values($refs);
}
private function getArgv($pid, $command) {
// In the output of "ps", arguments in process titles are not escaped, so
// we can not distinguish between the processes created by running these
// commands by looking only at the output of "ps":
//
// echo 'a b'
// echo a b
//
// Both commands will have the same process title in the output of "ps".
// This means we may split the command incorrectly in the general case,
// and this misparsing may be important if the process binary resides in
// a directory with spaces in its path and we're trying to identify which
// binary a process is running.
// On Ubuntu, and likely most other Linux systems, we can get a raw
// command line from "/proc" with arguments delimited by "\0".
// On macOS, there's no "/proc" and we don't currently have a robust way
// to split the process command in a way that parses spaces properly, so
// fall back to a best effort based on the output of "ps". This is almost
// always correct, since it is uncommon to put binaries under paths with
// spaces in them.
$proc_cmdline = sprintf('/proc/%d/cmdline', $pid);
try {
$argv = Filesystem::readFile($proc_cmdline);
$argv = explode("\0", $argv);
// The output itself is terminated with "\0", so remove the final empty
// argument.
if (last($argv) === '') {
array_pop($argv);
}
return $argv;
} catch (Exception $ex) {
// If we fail to read "/proc", fall through to less reliable methods.
}
// If we haven't found a better source, just split the "ps" output on
// spaces.
return preg_split('/\s+/', $command);
}
}

View file

@ -0,0 +1,85 @@
<?php
final class PhutilProcessRef
extends Phobject {
private $pid;
private $command;
private $isOverseer;
private $instance;
private $argv;
public function setPID($pid) {
$this->pid = $pid;
return $this;
}
public function getPID() {
return $this->pid;
}
public function getCommand() {
if (!$this->command) {
$this->command = phutil_string_cast(csprintf('%LR', $this->argv));
}
return $this->command;
}
public function getIsOverseer() {
if ($this->isOverseer === null) {
$this->isOverseer = $this->getCommandMatch(
array(
array('phd-daemon'),
array('php', 'phd-daemon'),
));
}
return $this->isOverseer;
}
public function setInstance($instance) {
$this->instance = $instance;
return $this;
}
public function getInstance() {
return $this->instance;
}
private function getCommandMatch(array $patterns) {
$argv = $this->getArgv();
foreach ($patterns as $pattern) {
$pattern = array_values($pattern);
$is_match = true;
for ($ii = 0; $ii < count($pattern); $ii++) {
if (!isset($argv[$ii])) {
$is_match = false;
break;
}
if (basename($argv[$ii]) !== $pattern[$ii]) {
$is_match = false;
break;
}
}
if ($is_match) {
return true;
}
}
return false;
}
public function setArgv(array $argv) {
$this->argv = $argv;
return $this;
}
public function getArgv() {
return $this->argv;
}
}

116
src/filesystem/TempFile.php Normal file
View file

@ -0,0 +1,116 @@
<?php
/**
* Simple wrapper to create a temporary file and guarantee it will be deleted on
* object destruction. Used like a string to path:
*
* $temp = new TempFile();
* Filesystem::writeFile($temp, 'Hello World');
* echo "Wrote data to path: ".$temp;
*
* Throws Filesystem exceptions for errors.
*
* @task create Creating a Temporary File
* @task config Configuration
* @task internal Internals
*/
final class TempFile extends Phobject {
private $dir;
private $file;
private $preserve;
private $destroyed = false;
/* -( Creating a Temporary File )------------------------------------------ */
/**
* Create a new temporary file.
*
* @param string? Filename hint. This is useful if you intend to edit the
* file with an interactive editor, so the user's editor shows
* "commit-message" instead of "p3810hf-1z9b89bas".
* @param string? Root directory to hold the file. If omitted, the system
* temporary directory (often "/tmp") will be used by default.
* @task create
*/
public function __construct($filename = null, $root_directory = null) {
$this->dir = Filesystem::createTemporaryDirectory(
'',
0700,
$root_directory);
if ($filename === null) {
$this->file = tempnam($this->dir, getmypid().'-');
} else {
$this->file = $this->dir.'/'.$filename;
}
// If we fatal (e.g., call a method on NULL), destructors are not called.
// Make sure our destructor is invoked.
register_shutdown_function(array($this, '__destruct'));
Filesystem::writeFile($this, '');
}
/* -( Configuration )------------------------------------------------------ */
/**
* Normally, the file is deleted when this object passes out of scope. You
* can set it to be preserved instead.
*
* @param bool True to preserve the file after object destruction.
* @return this
* @task config
*/
public function setPreserveFile($preserve) {
$this->preserve = $preserve;
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* Get the path to the temporary file. Normally you can just use the object
* in a string context.
*
* @return string Absolute path to the temporary file.
* @task internal
*/
public function __toString() {
return $this->file;
}
/**
* When the object is destroyed, it destroys the temporary file. You can
* change this behavior with @{method:setPreserveFile}.
*
* @task internal
*/
public function __destruct() {
if ($this->destroyed) {
return;
}
if ($this->preserve) {
return;
}
Filesystem::remove($this->dir);
// NOTE: tempnam() doesn't guarantee it will return a file inside the
// directory you passed to the function, so we make sure to nuke the file
// explicitly.
Filesystem::remove($this->file);
$this->file = null;
$this->dir = null;
$this->destroyed = true;
}
}

View file

@ -0,0 +1,232 @@
<?php
final class FileFinderTestCase extends PhutilTestCase {
private function newFinder($directory = null) {
if (!$directory) {
$directory = dirname(__FILE__).'/data';
}
return id(new FileFinder($directory))
->excludePath('./exclude')
->excludePath('subdir.txt');
}
public function testFinderWithChecksums() {
$this->assertFinder(
pht('Basic Checksums'),
$this->newFinder()
->setGenerateChecksums(true)
->withType('f')
->withPath('*')
->withSuffix('txt'),
array(
'.hidden.txt' =>
'b6cfc9ce9afe12b258ee1c19c235aa27',
'file.txt' =>
'725130ba6441eadb4e5d807898e0beae',
'include_dir.txt/anotherfile.txt' =>
'91e5c1ad76ff229c6456ac92e74e1d9f',
'include_dir.txt/subdir.txt/alsoinclude.txt' =>
'91e5c1ad76ff229c6456ac92e74e1d9f',
'test.txt' =>
'aea46212fa8b8d0e0e6aa34a15c9e2f5',
));
}
public function testFinderWithoutChecksums() {
$this->assertFinder(
pht('Basic No Checksums'),
$this->newFinder()
->withType('f')
->withPath('*')
->withSuffix('txt'),
array(
'.hidden.txt',
'file.txt',
'include_dir.txt/anotherfile.txt',
'include_dir.txt/subdir.txt/alsoinclude.txt',
'test.txt',
));
}
public function testFinderWithFilesAndDirectories() {
$this->assertFinder(
pht('With Files And Directories'),
$this->newFinder()
->setGenerateChecksums(true)
->withPath('*')
->withSuffix('txt'),
array(
'.hidden.txt' =>
'b6cfc9ce9afe12b258ee1c19c235aa27',
'file.txt' =>
'725130ba6441eadb4e5d807898e0beae',
'include_dir.txt' => null,
'include_dir.txt/anotherfile.txt' =>
'91e5c1ad76ff229c6456ac92e74e1d9f',
'include_dir.txt/subdir.txt' => null,
'include_dir.txt/subdir.txt/alsoinclude.txt' =>
'91e5c1ad76ff229c6456ac92e74e1d9f',
'test.txt' =>
'aea46212fa8b8d0e0e6aa34a15c9e2f5',
));
}
public function testFinderWithDirectories() {
$this->assertFinder(
pht('Just Directories'),
$this->newFinder()
->withType('d'),
array(
'include_dir.txt',
'include_dir.txt/subdir.txt',
));
}
public function testFinderWithPath() {
$this->assertFinder(
pht('With Path'),
$this->newFinder()
->setGenerateChecksums(true)
->withType('f')
->withPath('*/include_dir.txt/subdir.txt/alsoinclude.txt')
->withSuffix('txt'),
array(
'include_dir.txt/subdir.txt/alsoinclude.txt' =>
'91e5c1ad76ff229c6456ac92e74e1d9f',
));
}
public function testFinderWithNames() {
$this->assertFinder(
pht('With Names'),
$this->newFinder()
->withType('f')
->withPath('*')
->withName('test'),
array(
'include_dir.txt/subdir.txt/test',
'include_dir.txt/test',
'test',
));
}
public function testFinderWithNameAndSuffix() {
$this->assertFinder(
pht('With Name and Suffix'),
$this->newFinder()
->withType('f')
->withName('alsoinclude.txt')
->withSuffix('txt'),
array(
'include_dir.txt/subdir.txt/alsoinclude.txt',
));
}
public function testFinderWithGlobMagic() {
// Fill a temporary directory with all this magic garbage so we don't have
// to check a bunch of files with backslashes in their names into version
// control.
$tmp_dir = Filesystem::createTemporaryDirectory();
$crazy_magic = array(
'backslash\\.\\*',
'star-*.*',
'star-*.txt',
'star.t*t',
'star.tesseract',
);
foreach ($crazy_magic as $sketchy_path) {
Filesystem::writeFile($tmp_dir.'/'.$sketchy_path, '.');
}
$this->assertFinder(
pht('Glob Magic, Literal .t*t'),
$this->newFinder($tmp_dir)
->withType('f')
->withSuffix('t*t'),
array(
'star.t*t',
));
$this->assertFinder(
pht('Glob Magic, .tesseract'),
$this->newFinder($tmp_dir)
->withType('f')
->withSuffix('tesseract'),
array(
'star.tesseract',
));
$this->assertFinder(
pht('Glob Magic, Name'),
$this->newFinder($tmp_dir)
->withType('f')
->withName('star-*'),
array());
$this->assertFinder(
pht('Glob Magic, Name + Suffix'),
$this->newFinder($tmp_dir)
->withType('f')
->withName('star-*.*'),
array(
'star-*.*',
));
$this->assertFinder(
pht('Glob Magic, Backslash Suffix'),
$this->newFinder($tmp_dir)
->withType('f')
->withSuffix('\\*'),
array(
'backslash\\.\\*',
));
$this->assertFinder(
pht('Glob Magic, With Globs'),
$this->newFinder($tmp_dir)
->withType('f')
->withNameGlob('star-*'),
array(
'star-*.*',
'star-*.txt',
));
$this->assertFinder(
pht('Glob Magic, With Globs + Suffix'),
$this->newFinder($tmp_dir)
->withType('f')
->withNameGlob('star-*')
->withSuffix('txt'),
array(
'star-*.txt',
));
}
private function assertFinder($label, FileFinder $finder, $expect) {
$modes = array(
'php',
'shell',
);
foreach ($modes as $mode) {
$actual = id(clone $finder)
->setForceMode($mode)
->find();
if ($finder->getGenerateChecksums()) {
ksort($actual);
} else {
sort($actual);
}
$this->assertEqual(
$expect,
$actual,
pht('Test Case "%s" in Mode "%s"', $label, $mode));
}
}
}

Some files were not shown because too many files have changed in this diff Show more