Skip to content
Browse files

Initial import of libphutil.

  • Loading branch information...
0 parents commit a1b1d2f697aa80175bfe670964d01e11c05a108e @epriestley epriestley committed
Showing with 5,623 additions and 0 deletions.
  1. +6 −0 .arcconfig
  2. +4 −0 .divinerconfig
  3. +3 −0 .gitignore
  4. +13 −0 LICENSE
  5. +33 −0 README
  6. +10 −0 resources/git/commit-template.txt
  7. +78 −0 src/__phutil_library_init__.php
  8. +115 −0 src/__phutil_library_map__.php
  9. +163 −0 src/autoload/PhutilLibraryMapRegistry.php
  10. +20 −0 src/autoload/__init__.php
  11. +82 −0 src/autoload/autoload.php
  12. +93 −0 src/conduit/client/ConduitClient.php
  13. +32 −0 src/conduit/client/ConduitClientException.php
  14. +75 −0 src/conduit/client/ConduitFuture.php
  15. +23 −0 src/conduit/client/__init__.php
  16. +68 −0 src/console/PhutilConsoleFormatter.php
  17. +20 −0 src/console/__init__.php
  18. +232 −0 src/console/editor/PhutilInteractiveEditor.php
  19. +12 −0 src/console/editor/__init__.php
  20. +60 −0 src/console/format.php
  21. +10 −0 src/docs/overview.diviner
  22. +685 −0 src/filesystem/Filesystem.php
  23. +50 −0 src/filesystem/FilesystemException.php
  24. +14 −0 src/filesystem/__init__.php
  25. +148 −0 src/filesystem/filefinder/FileFinder.php
  26. +12 −0 src/filesystem/filefinder/__init__.php
  27. +89 −0 src/filesystem/filelist/FileList.php
  28. +21 −0 src/filesystem/filelist/__init__.php
  29. +69 −0 src/filesystem/tempfile/TempFile.php
  30. +20 −0 src/filesystem/tempfile/__init__.php
  31. +200 −0 src/future/Future.php
  32. +251 −0 src/future/FutureIterator.php
  33. +20 −0 src/future/__init__.php
  34. +53 −0 src/future/exec/CommandException.php
  35. +358 −0 src/future/exec/ExecFuture.php
  36. +16 −0 src/future/exec/__init__.php
  37. +73 −0 src/future/exec/execx.php
  38. +360 −0 src/future/http/HTTPFuture.php
  39. +21 −0 src/future/http/__init__.php
  40. +10 −0 src/markup/__init__.php
  41. +22 −0 src/markup/engine/PhutilMarkupEngine.php
  42. +10 −0 src/markup/engine/__init__.php
  43. +100 −0 src/markup/engine/remarkup/PhutilRemarkupEngine.php
  44. +14 −0 src/markup/engine/remarkup/__init__.php
  45. +3 −0 src/markup/engine/remarkup/__tests__/data/entities.txt
  46. +4 −0 src/markup/engine/remarkup/__tests__/data/leading-newline.txt
  47. +3 −0 src/markup/engine/remarkup/__tests__/data/link-brackets.txt
  48. +5 −0 src/markup/engine/remarkup/__tests__/data/link-with-punctuation.txt
  49. +3 −0 src/markup/engine/remarkup/__tests__/data/link-with-tilde.txt
  50. +3 −0 src/markup/engine/remarkup/__tests__/data/link.txt
  51. +8 −0 src/markup/engine/remarkup/__tests__/data/list.txt
  52. +3 −0 src/markup/engine/remarkup/__tests__/data/monospaced.txt
  53. +3 −0 src/markup/engine/remarkup/__tests__/data/simple.txt
  54. +57 −0 src/markup/engine/remarkup/blockrule/base/PhutilRemarkupEngineBlockRule.php
  55. +10 −0 src/markup/engine/remarkup/blockrule/base/__init__.php
  56. +48 −0 src/markup/engine/remarkup/blockrule/remarkupcode/PhutilRemarkupEngineRemarkupCodeBlockRule.php
  57. +12 −0 src/markup/engine/remarkup/blockrule/remarkupcode/__init__.php
  58. +36 −0 ...remarkup/blockrule/remarkupcounterexample/PhutilRemarkupEngineRemarkupCounterExampleBlockRule.php
  59. +12 −0 src/markup/engine/remarkup/blockrule/remarkupcounterexample/__init__.php
  60. +38 −0 ...markup/engine/remarkup/blockrule/remarkupdefault/PhutilRemarkupEngineRemarkupDefaultBlockRule.php
  61. +12 −0 src/markup/engine/remarkup/blockrule/remarkupdefault/__init__.php
  62. +42 −0 src/markup/engine/remarkup/blockrule/remarkupheader/PhutilRemarkupEngineRemarkupHeaderBlockRule.php
  63. +12 −0 src/markup/engine/remarkup/blockrule/remarkupheader/__init__.php
  64. +34 −0 src/markup/engine/remarkup/blockrule/remarkupinline/PhutilRemarkupEngineRemarkupInlineBlockRule.php
  65. +12 −0 src/markup/engine/remarkup/blockrule/remarkupinline/__init__.php
  66. +41 −0 src/markup/engine/remarkup/blockrule/remarkuplist/PhutilRemarkupEngineRemarkupListBlockRule.php
  67. +12 −0 src/markup/engine/remarkup/blockrule/remarkuplist/__init__.php
  68. +41 −0 src/markup/engine/remarkup/blockstorage/PhutilRemarkupBlockStorage.php
  69. +10 −0 src/markup/engine/remarkup/blockstorage/__init__.php
  70. +33 −0 src/markup/engine/remarkup/markuprule/base/PhutilRemarkupRule.php
  71. +10 −0 src/markup/engine/remarkup/markuprule/base/__init__.php
  72. +29 −0 src/markup/engine/remarkup/markuprule/bold/PhutilRemarkupRuleBold.php
  73. +12 −0 src/markup/engine/remarkup/markuprule/bold/__init__.php
  74. +26 −0 src/markup/engine/remarkup/markuprule/escapehtml/PhutilRemarkupRuleEscapeHTML.php
  75. +13 −0 src/markup/engine/remarkup/markuprule/escapehtml/__init__.php
  76. +33 −0 src/markup/engine/remarkup/markuprule/escaperemarkup/PhutilRemarkupRuleEscapeRemarkup.php
  77. +12 −0 src/markup/engine/remarkup/markuprule/escaperemarkup/__init__.php
  78. +59 −0 src/markup/engine/remarkup/markuprule/hyperlink/PhutilRemarkupRuleHyperlink.php
  79. +13 −0 src/markup/engine/remarkup/markuprule/hyperlink/__init__.php
  80. +29 −0 src/markup/engine/remarkup/markuprule/italics/PhutilRemarkupRuleItalic.php
  81. +12 −0 src/markup/engine/remarkup/markuprule/italics/__init__.php
  82. +29 −0 src/markup/engine/remarkup/markuprule/linebreaks/PhutilRemarkupRuleLinebreaks.php
  83. +12 −0 src/markup/engine/remarkup/markuprule/linebreaks/__init__.php
  84. +29 −0 src/markup/engine/remarkup/markuprule/monospace/PhutilRemarkupRuleMonospace.php
  85. +12 −0 src/markup/engine/remarkup/markuprule/monospace/__init__.php
  86. +36 −0 src/markup/render.php
  87. +12 −0 src/moduleutils/__init__.php
  88. +49 −0 src/moduleutils/moduleutils.php
  89. +19 −0 src/utils/__init__.php
  90. +205 −0 src/utils/utils.php
  91. +19 −0 src/xsprintf/__init__.php
  92. +21 −0 src/xsprintf/csprintf/__init__.php
  93. +88 −0 src/xsprintf/csprintf/csprintf.php
  94. +12 −0 src/xsprintf/jsprintf/__init__.php
  95. +104 −0 src/xsprintf/jsprintf/jsprintf.php
  96. +32 −0 src/xsprintf/qsprintf/QsprintfQueryParameterException.php
  97. +13 −0 src/xsprintf/qsprintf/__init__.php
  98. +346 −0 src/xsprintf/qsprintf/qsprintf.php
  99. +72 −0 src/xsprintf/xsprintf.php
6 .arcconfig
@@ -0,0 +1,6 @@
+{
+ "project_id" : "libphutil",
+ "conduit_uri" : "http://tools.epriestley-conduit.dev1557.facebook.com/api/",
+ "lint_engine" : "PhutilLintEngine",
+ "copyright_holder" : "Facebook, Inc."
+}
4 .divinerconfig
@@ -0,0 +1,4 @@
+{
+ "name" : "libphutil"
+}
+
3 .gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+._*
+docs/
13 LICENSE
@@ -0,0 +1,13 @@
+Copyright 2011 Facebook, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
33 README
@@ -0,0 +1,33 @@
+PROJECT STATUS: CAVEAT EMPTOR
+
+This is an unstable preview release. I'm open sourcing some of Facebook's
+internal tools, but they'll be unstable for at least a couple months.
+-epriestley
+
+
+WHAT IS LIBPHUTIL?
+
+libphutil is a collection of utility classes and functions for PHP.
+
+Futures
+ Futures (also known as "promises") are objects which act as placeholders for
+ some future result of computation. They let you express parallel and
+ asynchronous execution with a natural syntax. There are two provided concrete
+ Future implementations:
+ ExecFuture: execute system commands with a Future-based API
+ HTTPFuture: execute simple HTTP requests with a Future-based API
+ execx(): exception-based alternative to exec() with more capabilities
+
+Filesystem
+ The builtin PHP filesystem functions return error codes and emit warnings.
+ It is tedious to check these consistently. The Filesystem class provides a
+ simple API for common filesystem operations that throws exceptions on failure.
+
+xsprintf
+ This module allows you to build sprintf()-style functions that have arbitrary
+ conversions. This is particularly useful for escaping data correctly. Three
+ concrete implementations are provided:
+ csprintf: safely escape data for system commands
+ jsprintf: safely escape data for Javascript
+ qsprintf: safely escape data for MySQL
+
10 resources/git/commit-template.txt
@@ -0,0 +1,10 @@
+<<Enter Revision Title>>
+
+Summary:
+
+Test Plan:
+
+Reviewers:
+
+CC:
+
78 src/__phutil_library_init__.php
@@ -0,0 +1,78 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+define('__LIBPHUTIL__', true);
+
+function phutil_require_module($library, $module) {
+ phutil_module_stack('push', $library, $module);
+ phutil_require_source('__init__.php');
+ phutil_module_stack('pop');
+}
+
+function phutil_require_source($source) {
+ $base = phutil_module_stack('peek');
+ require_once $base.'/'.$source;
+}
+
+function phutil_module_exists($library, $module) {
+ $path = __phutil_library_registry('find', $library).$module.'/__init__.php';
+ return @file_exists($path);
+}
+
+function phutil_module_stack($op, $library = null, $module = null) {
+ static $stack = array();
+ switch ($op) {
+ case 'push':
+ $stack[] = __phutil_library_registry('find', $library).$module;
+ break;
+ case 'peek':
+ return end($stack);
+ case 'pop':
+ array_pop($stack);
+ break;
+ }
+}
+
+function phutil_register_library($library, $path) {
+ if (basename($path) != '__phutil_library_init__.php') {
+ throw new Exception(
+ 'Only directories with a __phutil_library_init__.php file may be '.
+ 'registered as libphutil libraries.');
+ }
+ return __phutil_library_registry('register', $library, dirname($path));
+}
+
+function phutil_load_library($path) {
+ require_once $path.'/__phutil_library_init__.php';
+}
+
+phutil_register_library('phutil', __FILE__);
+
+function __phutil_library_registry($op, $library = null, $path = null) {
+ static $dict = array();
+ switch ($op) {
+ case 'register':
+ $dict[$library] = $path.'/';
+ break;
+ case 'find':
+ return $dict[$library];
+ case 'list':
+ return $dict;
+ }
+}
+
115 src/__phutil_library_map__.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
+ * @generated
+ */
+
+phutil_register_library_map(array(
+ 'class' =>
+ array(
+ 'CommandException' => 'future/exec',
+ 'ConduitClient' => 'conduit/client',
+ 'ConduitClientException' => 'conduit/client',
+ 'ConduitFuture' => 'conduit/client',
+ 'ExecFuture' => 'future/exec',
+ 'FileFinder' => 'filesystem/filefinder',
+ 'FileList' => 'filesystem/filelist',
+ 'Filesystem' => 'filesystem',
+ 'FilesystemException' => 'filesystem',
+ 'Future' => 'future',
+ 'FutureIterator' => 'future',
+ 'HTTPFuture' => 'future/http',
+ 'PhutilConsoleFormatter' => 'console',
+ 'PhutilInteractiveEditor' => 'console/editor',
+ 'PhutilLibraryMapRegistry' => 'autoload',
+ 'PhutilMarkupEngine' => 'markup/engine',
+ 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/blockstorage',
+ 'PhutilRemarkupEngine' => 'markup/engine/remarkup',
+ 'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/base',
+ 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/remarkupcode',
+ 'PhutilRemarkupEngineRemarkupCounterExampleBlockRule' => 'markup/engine/remarkup/blockrule/remarkupcounterexample',
+ 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/remarkupdefault',
+ 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/remarkupheader',
+ 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/remarkupinline',
+ 'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/remarkuplist',
+ 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/base',
+ 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/bold',
+ 'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/escapehtml',
+ 'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/escaperemarkup',
+ 'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/hyperlink',
+ 'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/italics',
+ 'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/linebreaks',
+ 'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/monospace',
+ 'QsprintfQueryParameterException' => 'xsprintf/qsprintf',
+ 'TempFile' => 'filesystem/tempfile',
+ ),
+ 'function' =>
+ array(
+ 'Futures' => 'future',
+ '_qsprintf_check_scalar_type' => 'xsprintf/qsprintf',
+ '_qsprintf_check_type' => 'xsprintf/qsprintf',
+ 'array_select_keys' => 'utils',
+ 'coalesce' => 'utils',
+ 'csprintf' => 'xsprintf/csprintf',
+ 'exec_manual' => 'future/exec',
+ 'execx' => 'future/exec',
+ 'id' => 'utils',
+ 'idx' => 'utils',
+ 'ipull' => 'utils',
+ 'jsprintf' => 'xsprintf/jsprintf',
+ 'mgroup' => 'utils',
+ 'mpull' => 'utils',
+ 'msort' => 'utils',
+ 'mysql_escape_array_of_strings_for_in_clause' => 'xsprintf/qsprintf',
+ 'mysql_escape_column_name' => 'xsprintf/qsprintf',
+ 'mysql_escape_multiline_comment' => 'xsprintf/qsprintf',
+ 'newv' => 'utils',
+ 'nonempty' => 'utils',
+ 'phutil_autoload_class' => 'autoload',
+ 'phutil_autoload_function' => 'autoload',
+ 'phutil_console_confirm' => 'console',
+ 'phutil_console_format' => 'console',
+ 'phutil_console_prompt' => 'console',
+ 'phutil_console_wrap' => 'console',
+ 'phutil_escape_html' => 'markup',
+ 'phutil_find_class_descendants' => 'autoload',
+ 'phutil_find_classes_declared_in_module' => 'autoload',
+ 'phutil_get_library_name_for_root' => 'moduleutils',
+ 'phutil_get_library_root' => 'moduleutils',
+ 'phutil_get_library_root_for_path' => 'moduleutils',
+ 'phutil_register_library_map' => 'autoload',
+ 'phutil_render_tag' => 'markup',
+ 'qsprintf' => 'xsprintf/qsprintf',
+ 'vcsprintf' => 'xsprintf/csprintf',
+ 'vjsprintf' => 'xsprintf/jsprintf',
+ 'vqsprintf' => 'xsprintf/qsprintf',
+ 'xsprintf' => 'xsprintf',
+ 'xsprintf_command' => 'xsprintf/csprintf',
+ 'xsprintf_javascript' => 'xsprintf/jsprintf',
+ 'xsprintf_query' => 'xsprintf/qsprintf',
+ ),
+ 'requires_class' =>
+ array(
+ 'ConduitFuture' => 'HTTPFuture',
+ 'ExecFuture' => 'Future',
+ 'HTTPFuture' => 'Future',
+ 'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
+ 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupEngineRemarkupCounterExampleBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule',
+ 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule',
+ 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule',
+ ),
+ 'requires_interface' =>
+ array(
+ ),
+));
163 src/autoload/PhutilLibraryMapRegistry.php
@@ -0,0 +1,163 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class PhutilLibraryMapRegistry {
+
+ protected static $map = array();
+ protected static $library;
+
+ public static function findDescendantsOfClass($class) {
+ foreach (array_keys(__phutil_library_registry('list')) as $library) {
+ if (empty(self::$map[$library])) {
+ self::loadLibraryMap($library);
+ }
+ }
+ $children = array();
+ foreach (self::$map as $library => $map) {
+ foreach ($map['requires_class'] as $child => $parent) {
+ if ($parent == $class) {
+ $children[] = $child;
+ }
+ }
+ foreach ($map['requires_interface'] as $child => $parent) {
+ if ($parent == $class) {
+ $children[] = $child;
+ }
+ }
+ }
+
+ $descendants = array(
+ $children,
+ );
+ foreach ($children as $child) {
+ $descendants[] = self::findDescendantsOfClass($child);
+ }
+ return call_user_func_array('array_merge', $descendants);
+ }
+
+ public static function findClassesDeclaredInModule($library, $module) {
+ if (empty(self::$map[$library])) {
+ $map = self::loadLibraryMap($library);
+ } else {
+ $map = self::$map[$library];
+ }
+
+ $results = array();
+ foreach ($map['class'] as $class => $declared) {
+ if ($declared == $module) {
+ $results[] = $class;
+ }
+ }
+
+ return $results;
+ }
+
+ public static function register(array $map) {
+ self::$map[self::$library] = $map;
+ }
+
+ public static function loadLibraryMap($library) {
+ if (!empty(self::$map[$library])) {
+ return self::$map[$library];
+ }
+ self::$library = $library;
+ $root = __phutil_library_registry('find', $library);
+ $okay = @include_once $root.'/__phutil_library_map__.php';
+ if (!$okay) {
+ throw new Exception("Unable to load library map for '{$library}'.");
+ }
+ return self::$map[$library];
+ }
+
+ public static function findClass($library, $class, $search = true) {
+ return self::findSymbolOfType('class', $library, $class, $search);
+ }
+
+ public static function findFunction($library, $function, $search = true) {
+ return self::findSymbolOfType('function', $library, $function, $search);
+ }
+
+ public static function findSymbolOfType($type, $library, $symbol, $search) {
+ if ($library) {
+ $map = self::loadLibraryMap($library);
+ if (isset($map[$library][$type][$symbol])) {
+ return self::getSpec($type, $library, $symbol);
+ }
+ }
+
+ if (!$search) {
+ return false;
+ }
+
+ foreach (self::$map as $library => $map) {
+ if (isset($map[$type][$symbol])) {
+ return self::getSpec($type, $library, $symbol);
+ }
+ }
+
+ foreach (array_keys(__phutil_library_registry('list')) as $library) {
+ if (empty(self::$map[$library])) {
+ $map = self::loadLibraryMap($library);
+ if (isset($map[$type][$symbol])) {
+ return self::getSpec($type, $library, $symbol);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static function getSpec($type, $library, $symbol) {
+ if ($type == 'function') {
+ return self::getFunctionSpec($library, $symbol);
+ } else {
+ return self::getClassSpec($library, $symbol);
+ }
+ }
+
+ private static function getFunctionSpec($library, $function) {
+ $map = self::$map[$library];
+ return array(
+ 'library' => $library,
+ 'module' => $map['function'][$function],
+ );
+ }
+
+ private static function getClassSpec($library, $class) {
+ $map = self::$map[$library];
+ $module = $map['class'][$class];
+ $parent = null;
+ $interface = array();
+
+ if (!empty($map['requires_class'][$class])) {
+ $parent = $map['requires_class'][$class];
+ }
+
+ if (!empty($map['requires_interface'][$class])) {
+ $interface = $map['requires_interface'][$class];
+ }
+
+ return array(
+ 'library' => $library,
+ 'module' => $module,
+ 'requires_class' => $parent,
+ 'requires_interface' => $interface,
+ );
+ }
+
+}
20 src/autoload/__init__.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+phutil_require_source('autoload.php');
+phutil_require_source('PhutilLibraryMapRegistry.php');
82 src/autoload/autoload.php
@@ -0,0 +1,82 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function phutil_autoload_class($class, $library = null) {
+ if (class_exists($class, false) || interface_exists($class, false)) {
+ return true;
+ }
+
+ $spec = PhutilLibraryMapRegistry::findClass($library, $class, $search = true);
+
+ if (!$spec) {
+ return false;
+ }
+
+ if ($spec['requires_class']) {
+ if (!class_exists($spec['requires_class'], $autoload = false)) {
+ phutil_autoload_class($spec['requires_class'], $spec['library']);
+ }
+ }
+
+ foreach ($spec['requires_interface'] as $interface) {
+ if (!interface_exists($interface, $autoload = false)) {
+ phutil_autoload_class($interface, $spec['library']);
+ }
+ }
+
+ // Foil static analysis.
+ $require = 'phutil_require_module';
+ $require($spec['library'], $spec['module']);
+
+ return (class_exists($class, false) || interface_exists($class, false));
+}
+
+function phutil_autoload_function($function, $library = null) {
+ if (function_exists($function)) {
+ return true;
+ }
+
+ $spec = PhutilLibraryMapRegistry::findFunction(
+ $library,
+ $function,
+ $search = true);
+
+ if (!$spec) {
+ return false;
+ }
+
+ // Foil static analysis.
+ $require = 'phutil_require_module';
+ $require($spec['library'], $spec['module']);
+
+ return function_exists($function);
+}
+
+function phutil_find_class_descendants($class) {
+ return PhutilLibraryMapRegistry::findDescendantsOfClass($class);
+}
+
+function phutil_find_classes_declared_in_module($library, $module) {
+ return PhutilLibraryMapRegistry::findClassesDeclaredInModule(
+ $library,
+ $module);
+}
+
+function phutil_register_library_map(array $map) {
+ PhutilLibraryMapRegistry::register($map);
+}
93 src/conduit/client/ConduitClient.php
@@ -0,0 +1,93 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class ConduitClient {
+
+ protected $host;
+ protected $path;
+ protected $traceMode;
+ protected $connectionID;
+
+ public function setConnectionID($connection_id) {
+ $this->connectionID = $connection_id;
+ return $this;
+ }
+
+ public function getConnectionID() {
+ return $this->connectionID;
+ }
+
+ public function __construct($uri) {
+ $this->host = parse_url($uri, PHP_URL_HOST);
+ $this->path = parse_url($uri, PHP_URL_PATH);
+
+ if (!$this->host) {
+ throw new Exception("Conduit URI '{$uri}' must include a valid host.");
+ }
+
+ $this->path = trim($this->path, '/').'/';
+ }
+
+ public function callMethodSynchronous($method, array $params) {
+ return $this->callMethod($method, $params)->resolve();
+ }
+
+ public function callMethod($method, array $params) {
+
+ $meta = array();
+ if ($this->getConnectionID()) {
+ $meta['connectionID'] = $this->getConnectionID();
+ }
+
+ if ($meta) {
+ $params['__conduit__'] = $meta;
+ }
+
+ $start_time = microtime(true);
+ $future = new ConduitFuture(
+ 'http://'.$this->host.'/'.$this->path.$method,
+ array(
+ 'params' => json_encode($params),
+ 'output' => 'json',
+ ));
+ $future->setMethod('POST');
+ $future->isReady();
+
+ if ($this->getTraceMode()) {
+ $future_name = $method;
+ $future->setTraceMode(true);
+ $future->setStartTime($start_time);
+ $future->setTraceName($future_name);
+ echo "[Conduit] >>> Send {$future_name}()...\n";
+ }
+
+ return $future;
+ }
+
+ public function setTraceMode($mode) {
+ $this->traceMode = $mode;
+ return $this;
+ }
+
+ protected function getTraceMode() {
+ if (!empty($this->traceMode)) {
+ return true;
+ }
+ return false;
+ }
+}
32 src/conduit/client/ConduitClientException.php
@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class ConduitClientException extends Exception {
+
+ protected $errorCode;
+
+ public function __construct($code, $info) {
+ parent::__construct("{$code}: {$info}");
+ $this->errorCode = $code;
+ }
+
+ public function getErrorCode() {
+ return $this->errorCode;
+ }
+
+}
75 src/conduit/client/ConduitFuture.php
@@ -0,0 +1,75 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class ConduitFuture extends HTTPFuture {
+
+ protected $traceMode;
+ protected $traceName;
+ protected $startTime;
+ protected $endTime;
+
+ public function setTraceMode($trace_mode) {
+ $this->traceMode = $trace_mode;
+ return $this;
+ }
+
+ public function setTraceName($trace_name) {
+ $this->traceName = $trace_name;
+ return $this;
+ }
+
+ public function setStartTime($time) {
+ $this->startTime = $time;
+ return $this;
+ }
+
+ protected function getResult() {
+ $result = parent::getResult();
+
+ if (empty($this->endTime)) {
+ $this->endTime = microtime(true);
+ $time = (int)(1000 * ($this->endTime - $this->startTime));
+ $time = number_format($time).' ms';
+ if ($this->traceMode) {
+ echo "[Conduit] <<< Completed {$this->traceName} in {$time}.\n";
+ }
+ }
+
+ if ($result[0] !== 200) {
+ throw new Exception(
+ "Host returned an HTTP error response #{$result[0]} in response ".
+ "to a Conduit method call.");
+ }
+
+ $data = json_decode($result[1], true);
+ if (!is_array($data)) {
+ throw new Exception(
+ "Host returned HTTP/200, but invalid JSON data in response to ".
+ "a Conduit method call:\n{$result[1]}");
+ }
+
+ if ($data['error_code']) {
+ throw new ConduitClientException(
+ $data['error_code'],
+ $data['error_info']);
+ }
+
+ return $data['result'];
+ }
+
+}
23 src/conduit/client/__init__.php
@@ -0,0 +1,23 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+phutil_require_module('phutil', 'future/http');
+
+phutil_require_source('ConduitClient.php');
+phutil_require_source('ConduitFuture.php');
+phutil_require_source('ConduitClientException.php');
68 src/console/PhutilConsoleFormatter.php
@@ -0,0 +1,68 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class PhutilConsoleFormatter {
+
+ private static $colorCodes = array(
+ 'black' => 0,
+ 'red' => 1,
+ 'green' => 2,
+ 'yellow' => 3,
+ 'blue' => 4,
+ 'magenta' => 5,
+ 'cyan' => 6,
+ 'white' => 7,
+ 'default' => 9,
+ );
+
+
+ public static function formatString($format /* ... */) {
+ $esc = chr(27);
+ $bold = $esc.'[1m'.'\\1'.$esc.'[m';
+ $underline = $esc.'[4m'.'\\1'.$esc.'[m';
+ $invert = $esc.'[7m'.'\\1'.$esc.'[m';
+
+ $colors = implode('|', array_keys(self::$colorCodes));
+
+ $format = preg_replace('/\*\*(.*)\*\*/sU', $bold, $format);
+ $format = preg_replace('/__(.*)__/sU', $underline, $format);
+ $format = preg_replace('/##(.*)##/sU', $invert, $format);
+ $format = preg_replace_callback(
+ '@<(fg|bg):('.$colors.')>(.*)</\1>@sU',
+ array('PhutilConsoleFormatter', 'replaceColorCode'),
+ $format);
+
+ $args = func_get_args();
+ $args[0] = $format;
+
+ 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';
+ }
+
+}
20 src/console/__init__.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+phutil_require_source('format.php');
+phutil_require_source('PhutilConsoleFormatter.php');
232 src/console/editor/PhutilInteractiveEditor.php
@@ -0,0 +1,232 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * 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 {
+
+ private $name = '';
+ private $content = '';
+ private $offset = 0;
+ private $fallback = 'nano';
+
+
+/* -( 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();
+
+ $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();
+
+ $arg_editor = $editor;
+ $arg_offset = escapeshellarg($offset);
+ $arg_path = escapeshellarg($path);
+
+ $err = 0;
+ passthru("{$arg_editor} +{$arg_offset} {$arg_path}", $err);
+
+ if ($err) {
+ Filesystem::remove($tmp);
+ throw new Exception("Editor exited with an error code (#{$err}).");
+ }
+
+ try {
+ $result = Filesystem::readFile($path);
+ Filesystem::remove($tmp);
+ } catch (Exception $ex) {
+ Filesystem::remove($tmp);
+ throw $ex;
+ }
+
+ $this->setContent($result);
+
+ return $this->getContent();
+ }
+
+
+/* -( 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 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.
+ *
+ * @param string Command-line editing program (e.g. 'emacs', 'vi')
+ * @return $this
+ *
+ * @task config
+ */
+ public function setFallbackEditor($editor) {
+ $this->fallback = $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 best editor
+ * will be selected.
+ *
+ * @return string Command-line editing program.
+ *
+ * @task config
+ */
+ public function getEditor() {
+ $editor = getenv('EDITOR');
+ if (!$editor) {
+ $editor = $this->fallback;
+ }
+
+ return $editor;
+ }
+}
12 src/console/editor/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phutil', 'filesystem');
+
+
+phutil_require_source('PhutilInteractiveEditor.php');
60 src/console/format.php
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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_prompt($prompt) {
+
+ $prompt = "\n\n ".$prompt." ";
+ $prompt = phutil_console_wrap($prompt, 4);
+
+ echo $prompt;
+ $response = fgets(STDIN);
+
+ return rtrim($response, "\n");
+}
+
+function phutil_console_wrap($text, $indent = 0) {
+ $indent_string = '';
+ if ($indent) {
+ $indent_string = str_repeat(' ', $indent);
+ }
+ return wordwrap($text, 78 - $indent, "\n".$indent_string);
+}
10 src/docs/overview.diviner
@@ -0,0 +1,10 @@
+@title libphutil Overview
+
+This document provides a high-level introduction to libphutil.
+
+= Overview =
+
+libphutil is a collection of PHP utility classes and modules.
+
+This project is in an unstable state. See @{article:Project Status} for a
+description of the project status.
685 src/filesystem/Filesystem.php
@@ -0,0 +1,685 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Simple wrapper class for common filesystem tasks like reading and writing
+ * files. When things go wrong, this class throws detailed exceptions with
+ * good information about what didn't work.
+ *
+ * Filesystem will resolve relative paths against PWD from the environment.
+ * When Filesystem is unable to complete an operation, it throws a
+ * FilesystemException.
+ *
+ * @task directory Directories
+ * @task file Files
+ * @task path Paths
+ * @task assert Assertions
+ */
+class Filesystem {
+
+
+/* -( Files )-------------------------------------------------------------- */
+
+
+ /**
+ * Read a file in a manner similar to file_get_contents(), but throw detailed
+ * exceptions on failure.
+ *
+ * @param string File path to read. This file must exist and be readable,
+ * or an exception will be thrown.
+ * @return string Contents of the specified file.
+ *
+ * @task file
+ */
+ public static function readFile($path) {
+ $path = self::resolvePath($path);
+
+ self::assertExists($path);
+ self::assertIsFile($path);
+ self::assertReadable($path);
+
+ $data = @file_get_contents($path);
+ if ($data === false) {
+ throw new FilesystemException(
+ $path,
+ "Failed to read file `{$path}'.");
+ }
+
+ return $data;
+ }
+
+
+ /**
+ * Write a file in a manner similar to file_put_contents(), but throw
+ * detailed exceptions on failure. If the file already exists, it will be
+ * overwritten.
+ *
+ * @param string File path to write. This file must be writable and its
+ * parent directory must exist.
+ * @param string Data to write.
+ *
+ * @task file
+ */
+ public static function writeFile($path, $data) {
+ $path = self::resolvePath($path);
+ $dir = dirname($path);
+
+ self::assertExists($dir);
+ self::assertIsDirectory($dir);
+
+ // File either needs to not exist and have a writable parent, or be
+ // writable itself.
+ $exists = true;
+ try {
+ self::assertNotExists($path);
+ $exists = false;
+ } catch (Exception $ex) {
+ self::assertWritable($path);
+ }
+
+ if (!$exists) {
+ self::assertWritable($dir);
+ }
+
+ if (@file_put_contents($path, $data) === false) {
+ throw new FilesystemException(
+ $path,
+ "Failed to write file `{$path}'.");
+ }
+ }
+
+
+ /**
+ * Append to a file without having to deal with file handles, with
+ * detailed exceptions on failure.
+ *
+ * @param string File path to write. This file must be writable or its
+ * parent directory must exist and be writable.
+ * @param string Data to write.
+ *
+ * @task file
+ */
+ public static function appendFile($path, $data) {
+ $path = self::resolvePath($path);
+
+ // Use self::writeFile() if the file doesn't already exist
+ try {
+ self::assertExists($path);
+ } catch (FilesystemException $ex) {
+ self::writeFile($path, $data);
+ return;
+ }
+
+ // File needs to exist or the directory needs to be writable
+ $dir = dirname($path);
+ self::assertExists($dir);
+ self::assertIsDirectory($dir);
+ self::assertWritable($dir);
+
+ if (($fh = fopen($path, 'a')) === false) {
+ throw new FilesystemException(
+ $path, "Failed to open file `{$path}'.");
+ }
+ $dlen = strlen($data);
+ if (fwrite($fh, $data) !== $dlen) {
+ throw new FilesystemException(
+ $path,
+ "Failed to write {$dlen} bytes to `{$path}'.");
+ }
+ if (!fflush($fh) || !fclose($fh)) {
+ throw new FilesystemException(
+ $path,
+ "Failed closing file `{$path}' after write.");
+ }
+ }
+
+
+ /**
+ * Remove a file or directory.
+ *
+ * @param string File to a path or directory to remove.
+ * @return void
+ *
+ * @task file
+ */
+ public static function remove($path) {
+ if (!strlen($path)) {
+ // Avoid removing PWD.
+ throw new Exception("No path provided to remove().");
+ }
+
+ $path = self::resolvePath($path);
+
+ if (!file_exists($path)) {
+ return;
+ }
+
+ // PHP's builtin functions for this are such a mess that it's much easier
+ // to exec out. :/
+ list($err, $stdout, $stderr) = exec_manual('rm -rf %s', $path);
+ if ($err) {
+ throw new FilesystemException(
+ $path,
+ "Unable to remove `{$path}'.");
+ }
+ }
+
+
+ /**
+ * Change the permissions of a file or directory.
+ *
+ * @param string Path to the file or directory.
+ * @param int Permission umask. Note that umask is in octal, so you
+ * should specify it as, e.g., `0777', not `777'.
+ * @return void
+ *
+ * @task file
+ */
+ public static function changePermissions($path, $umask) {
+ $path = self::resolvePath($path);
+
+ self::assertExists($path);
+
+ if (!@chmod($path, $umask)) {
+ $readable_umask = sprintf("%04o", $umask);
+ throw new FilesystemException(
+ $path, "Failed to chmod `{$path}' to `{$readable_umask}'.");
+ }
+ }
+
+
+ /**
+ * Get the last modified time of a file
+ *
+ * @param string Path to file
+ * @return Time last modified
+ *
+ * @task file
+ */
+ public static function getModifiedTime($path) {
+ $path = self::resolvePath($path);
+ self::assertExists($path);
+ self::assertIsFile($path);
+ self::assertReadable($path);
+
+ $modified_time = @filemtime($path);
+
+ if ($modified_time === false) {
+ throw new FilesystemException(
+ $path,
+ 'Failed to read modified time for '.$path);
+ }
+
+ return $modified_time;
+ }
+
+
+ /**
+ * Determine if a file is an image, approximately. This is a simple path
+ * test which exists primarily to reduce code duplication.
+ *
+ * @param string Path in question.
+ * @return bool True if the corresponding file is (probably) an image.
+ *
+ * @task file
+ */
+ public static function isImageFilename($path) {
+ $path = self::resolvePath($path);
+ return (bool)preg_match('/[.](jpe?g|gif|png)$/i', $path);
+ }
+
+
+/* -( Directories )-------------------------------------------------------- */
+
+
+ /**
+ * Create a directory in a manner similar to mkdir(), but throw detailed
+ * exceptions on failure.
+ *
+ * @param string Path to directory. The parent directory must exist and
+ * be writable.
+ * @param int Permission umask. Note that umask is in octal, so you
+ * should specify it as, e.g., `0777', not `777'. By
+ * default, these permissions are very liberal (0777).
+ * @param boolean Recursivly create directories. Default to false
+ * @return string Path to the created directory.
+ *
+ * @task directory
+ */
+ public static function createDirectory($path, $umask = 0777,
+ $recursive = false) {
+ $path = self::resolvePath($path);
+
+ if (is_dir($path)) {
+ Filesystem::changePermissions($path, $umask);
+ return $path;
+ }
+
+ $dir = dirname($path);
+ if ($recursive && !file_exists($dir)) {
+ // Note: We could do this with the recursive third parameter of mkdir(),
+ // but then we loose the helpful FilesystemExceptions we normally get.
+ self::createDirectory($dir, $umask, true);
+ }
+
+ self::assertIsDirectory($dir);
+ self::assertExists($dir);
+ self::assertWritable($dir);
+ self::assertNotExists($path);
+
+ if (!mkdir($path, $umask)) {
+ throw new FilesystemException(
+ $path,
+ "Failed to create directory `{$path}'.");
+ }
+
+ // Need to change premissions explicitly because mkdir does something
+ // slightly different. mkdir(2) man page:
+ // 'The parameter mode specifies the permissions to use. It is modified by
+ // the process's umask in the usual way: the permissions of the created
+ // directory are (mode & ~umask & 0777)."'
+ Filesystem::changePermissions($path, $umask);
+
+ return $path;
+ }
+
+
+ /**
+ * Create a temporary directory and return the path to it. You are
+ * responsible for removing it (e.g., with Filesystem::remove())
+ * when you are done with it.
+ *
+ * @param string Optional directory prefix.
+ * @param int Permissions to create the directory with. By default,
+ * these permissions are very restrictive (0700).
+ * @return string Path to newly created temporary directory.
+ *
+ * @task directory
+ */
+ public static function createTemporaryDirectory($prefix = '', $umask = 0700) {
+ $prefix = preg_replace('/[^A-Z0-9._-]+/i', '', $prefix);
+
+ $tmp = sys_get_temp_dir();
+ if (!$tmp) {
+ throw new FilesystemException(
+ $tmp, 'Unable to determine system temporary directory.');
+ }
+
+ $base = $tmp.DIRECTORY_SEPARATOR.$prefix;
+
+ $tries = 3;
+ do {
+ $dir = $base.substr(base_convert(md5(mt_rand()), 16, 36), 0, 16);
+ try {
+ self::createDirectory($dir, $umask);
+ break;
+ } catch (FilesystemException $ex) {
+ // Ignore.
+ }
+ } while (--$tries);
+
+ if (!$tries) {
+
+ $df = disk_free_space($tmp);
+ if ($df < 1024 * 1024) {
+ throw new FilesystemException(
+ $dir, "Failed to create a temporary directory: the disk is full.");
+ }
+
+ throw new FilesystemException(
+ $dir, "Failed to create a temporary directory.");
+ }
+
+ return $dir;
+ }
+
+
+ /**
+ * List files in a directory.
+ *
+ * @param string Path, absolute or relative to PWD.
+ * @param bool If false, exclude files beginning with a ".".
+ *
+ * @return array List of files and directories in the specified
+ * directory, excluding `.' and `..'.
+ *
+ * @task directory
+ */
+ public static function listDirectory($path, $include_hidden = true) {
+ $path = self::resolvePath($path);
+
+ self::assertExists($path);
+ self::assertIsDirectory($path);
+ self::assertReadable($path);
+
+ $list = @scandir($path);
+ if ($list === false) {
+ throw new Exception(
+ $path,
+ "Unable to list contents of directory `{$path}'.");
+ }
+
+ foreach ($list as $k => $v) {
+ if ($v == '.' || $v == '..' || (!$include_hidden && $v[0] == '.')) {
+ unset($list[$k]);
+ }
+ }
+
+ return array_values($list);
+ }
+
+
+ /**
+ * Return all directories between a path and "/". Iterating over them walks
+ * from the path to the root.
+ *
+ * @param string Path, absolute or relative to PWD.
+ * @return list List of parent paths, including the provided path.
+ * @task directory
+ */
+ public static function walkToRoot($path) {
+ $path = self::resolvePath($path);
+ if ($path == '/') {
+ return array('/');
+ }
+
+ $walk = array();
+ $parts = explode('/', $path);
+ foreach ($parts as $k => $part) {
+ if (!strlen($part)) {
+ unset($parts[$k]);
+ }
+ }
+ do {
+ $walk[] = '/'.implode('/', $parts);
+ if (empty($parts)) {
+ break;
+ }
+ array_pop($parts);
+ } while (true);
+
+ return $walk;
+ }
+
+
+/* -( Paths )-------------------------------------------------------------- */
+
+
+ /**
+ * Canonicalize a path by resolving it relative to some directory (by
+ * default PWD), following parent symlinks and removing artifacts. If the
+ * path is itself a symlink it is left unresolved.
+ *
+ * @param string Path, absolute or relative to PWD.
+ * @return string Canonical, absolute path.
+ *
+ * @task path
+ */
+ public static function resolvePath($path, $relative_to = null) {
+ if (strncmp($path, '/', 1)) {
+ if (!$relative_to) {
+ $relative_to = $_SERVER['PWD'];
+ }
+ $path = $relative_to.'/'.$path;
+ }
+
+ if (is_link($path)) {
+ $parent_realpath = realpath(dirname($path));
+ if ($parent_realpath !== false) {
+ return $parent_realpath.'/'.basename($path);
+ }
+ }
+
+ $realpath = realpath($path);
+ if ($realpath !== false) {
+ return $realpath;
+ }
+
+ // This won't work if the file doesn't exist or is on an unreadable mount
+ // or something crazy like that. Try to resolve a parent so we at least
+ // cover the nonexistent file case.
+ $parts = explode('/', trim($path, '/'));
+ while (end($parts) !== false) {
+ array_pop($parts);
+ $attempt = '/'.implode('/', $parts);
+ $realpath = realpath($attempt);
+ if ($realpath !== false) {
+ $path = $realpath.substr($path, strlen($attempt));
+ break;
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * Test whether a path is descendant from some root path after resolving all
+ * symlinks and removing artifacts. Both paths must exists for the relation
+ * to obtain. A path is always a descendant of itself as long as it exists.
+ *
+ * @param string Child path, absolute or relative to PWD.
+ * @param string Root path, absolute or relative to PWD.
+ * @return bool True if resolved child path is in fact a descendant of
+ * resolved root path and both exist.
+ * @task path
+ */
+ public static function isDescendant($path, $root) {
+
+ try {
+ self::assertExists($path);
+ self::assertExists($root);
+ } catch (FilesystemException $e) {
+ return false;
+ }
+ $fs = new FileList(array($root));
+ return $fs->contains($path);
+ }
+
+ /**
+ * Convert a canonical path to its most human-readable format. It is
+ * guaranteed that you can use resolvePath() to restore a path to its
+ * canonical format.
+ *
+ * @param string Path, absolute or relative to PWD.
+ * @param string Optionally, working directory to make files readable
+ * relative to.
+ * @return string Human-readable path.
+ *
+ * @task path
+ */
+ public static function readablePath($path, $pwd = null) {
+ if ($pwd === null) {
+ $pwd = $_SERVER['PWD'];
+ }
+
+ foreach (array($pwd, self::resolvePath($pwd)) as $parent) {
+ $parent = rtrim($parent, '/').'/';
+ $len = strlen($parent);
+ if (!strncmp($parent, $path, $len)) {
+ $path = substr($path, $len);
+ return $path;
+ }
+ }
+
+ return $path;
+ }
+
+ /**
+ * Determine whether or not a path exists in the filesystem. This differs from
+ * file_exists() in that it returns true for symlinks. This method does not
+ * attempt to resolve paths before testing them.
+ *
+ * @param string Test for the existence of this path.
+ * @return bool True if the path exists in the filesystem.
+ * @task path
+ */
+ public static function pathExists($path) {
+ return file_exists($path) || is_link($path);
+ }
+
+
+ /**
+ * Determine if two paths are equivalent by resolving symlinks. This is
+ * different from resolving both paths and comparing them because
+ * resolvePath() only resolves symlinks in parent directories, not the
+ * path itself.
+ *
+ * @param string First path to test for equivalence.
+ * @param string Second path to test for equivalence.
+ * @return bool True if both paths are equivalent, i.e. reference the same
+ * entity in the filesystem.
+ * @task path
+ */
+ public static function pathsAreEquivalent($u, $v) {
+ $u = Filesystem::resolvePath($u);
+ $v = Filesystem::resolvePath($v);
+
+ $real_u = realpath($u);
+ $real_v = realpath($v);
+
+ if ($real_u) {
+ $u = $real_u;
+ }
+ if ($real_v) {
+ $v = $real_v;
+ }
+ return ($u == $v);
+ }
+
+
+/* -( Assert )------------------------------------------------------------- */
+
+ /**
+ * Assert that something (e.g., a file, directory, or symlink) is an
+ * absolute path to the specified location.
+ *
+ * @param string Assert that this path is absolute.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertAbsolute($path) {
+ if (empty($path) || $path[0] != '/') {
+ throw new FilesystemException(
+ $path,
+ "Filesystem entity `{$path}' is not absolute.");
+ }
+ }
+
+ /**
+ * Assert that something (e.g., a file, directory, or symlink) exists at a
+ * specified location.
+ *
+ * @param string Assert that this path exists.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertExists($path) {
+ if (!self::pathExists($path)) {
+ throw new FilesystemException(
+ $path,
+ "Filesystem entity `{$path}' does not exist.");
+ }
+ }
+
+
+ /**
+ * Assert that nothing exists at a specified location.
+ *
+ * @param string Assert that this path does not exist.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertNotExists($path) {
+ if (file_exists($path) || is_link($path)) {
+ throw new FilesystemException(
+ $path,
+ "Path `{$path}' already exists!");
+ }
+ }
+
+
+ /**
+ * Assert that a path represents a file, strictly (i.e., not a directory).
+ *
+ * @param string Assert that this path is a file.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertIsFile($path) {
+ if (!is_file($path)) {
+ throw new FilesystemException(
+ $path,
+ "Requested path `{$path}' is not a file.");
+ }
+ }
+
+
+ /**
+ * Assert that a path represents a directory, strictly (i.e., not a file).
+ *
+ * @param string Assert that this path is a directory.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertIsDirectory($path) {
+ if (!is_dir($path)) {
+ throw new FilesystemException(
+ $path,
+ "Requested path `{$path}' is not a directory.");
+ }
+ }
+
+
+ /**
+ * Assert that a file or directory exists and is writable.
+ *
+ * @param string Assert that this path is writable.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertWritable($path) {
+ if (!is_writable($path)) {
+ throw new FilesystemException(
+ $path,
+ "Requested path `{$path}' is not writable.");
+ }
+ }
+
+
+ /**
+ * Assert that a file or directory exists and is readable.
+ *
+ * @param string Assert that this path is readable.
+ * @return void
+ *
+ * @task assert
+ */
+ public static function assertReadable($path) {
+ if (!is_readable($path)) {
+ throw new FilesystemException(
+ $path,
+ "Path `{$path}' is not readable.");
+ }
+ }
+}
50 src/filesystem/FilesystemException.php
@@ -0,0 +1,50 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Exception thrown by Filesystem to indicate an error accessing the file
+ * system.
+ */
+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;
+ }
+
+}
14 src/filesystem/__init__.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phutil', 'filesystem/filelist');
+phutil_require_module('phutil', 'future/exec');
+
+
+phutil_require_source('Filesystem.php');
+phutil_require_source('FilesystemException.php');
148 src/filesystem/filefinder/FileFinder.php
</
@@ -0,0 +1,148 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * PHP API for `find`.
+ */
+class FileFinder {
+
+ protected $root;
+ protected $exclude = array();
+ protected $paths = array();
+ protected $suffix = array();
+ protected $type;
+ protected $generateChecksums = false;
+
+ public function __construct($root) {
+ $this->root = $root;
+ }
+
+ public function excludePath($path) {
+ $this->exclude[] = $path;
+ return $this;
+ }
+
+ public function withSuffix($suffix) {
+ $this->suffix[] = '*.'.$suffix;
+ return $this;
+ }
+
+ public function withPath($path) {
+ $this->paths[] = $path;
+ return $this;
+ }
+
+ public function withType($type) {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function setGenerateChecksums($generate) {
+ $this->generateChecksums = $generate;
+ return $this;
+ }
+
+ public function find() {
+ $args = array();
+ $command = array();
+
+ $command[] = '(cd %s; ';
+ $args[] = $this->root;
+
+ $command[] = 'find .';
+
+ if ($this->exclude) {
+ $command[] = $this->generateList('path', $this->exclude).' -prune';
+ $command[] = '-o';
+ }
+
+ if ($this->type) {
+ $command[] = '-type %s';
+ $args[] = $this->type;
+ }
+
+ if ($this->suffix) {
+ $command[] = $this->generateList('name', $this->suffix);
+ }
+
+ if ($this->paths) {
+ $command[] = $this->generateList('wholename', $this->paths);
+ }
+
+ $command[] = '-print0';
+
+ if ($this->generateChecksums) {
+ static $md5sum_binary = null;
+ if ($md5sum_binary == null) {
+
+ $options = array(
+ 'md5sum' => 'md5sum',
+ 'md5' => 'md5 -r',
+ );
+ foreach ($options as $bin => $choose) {
+ list($err) = exec_manual('which %s', $bin);
+ if ($err == 0) {
+ $md5sum_binary = $choose;
+ break;
+ }
+ }
+ if ($md5sum_binary === null) {
+ throw new Exception(