diff --git a/.arcconfig b/.arcconfig index 033afee..82fb487 100644 --- a/.arcconfig +++ b/.arcconfig @@ -1,7 +1,6 @@ { "project_id" : "libphutil", "conduit_uri" : "https://secure.phabricator.com/api/", "lint.engine" : "PhutilLintEngine", - "unit.engine" : "PhutilUnitTestEngine", - "copyright_holder" : "Facebook, Inc." + "unit.engine" : "PhutilUnitTestEngine" } diff --git a/LICENSE b/LICENSE index 60686e6..f433b1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,177 @@ -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 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -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. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..bf6fbb3 --- /dev/null +++ b/NOTICE @@ -0,0 +1,5 @@ +libphutil +Copyright 2012 Facebook, Inc. + +This product includes software developed at +Facebook, Inc. (http://www.facebook.com/facebook). diff --git a/README b/README index 2ab9261..0759f45 100644 --- a/README +++ b/README @@ -1,45 +1,49 @@ WHAT IS LIBPHUTIL? libphutil is a collection of utility classes and functions for PHP. Some features of the library include: libphutil Library System A system for organizing, loading and introspecting PHP classes and functions. Uses static analysis to generate, validate and update library contents and includes. Based on Facebook's similar 'flib' system. 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 for executing system commands, and HTTPFuture for making HTTP requests. 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 (in Phabricator) AAST/PHPAST An abstract, abstract syntax tree which can make it easier to perform simple static analysis, and a concrete AST for PHP. Remarkup A Markdown-like lightweight markup language. Remarkup's syntax is defined by parser plugins and fairly easy to extend and configure. Daemons Enables running PHP scripts as stable, long-lived daemons. Utilities A handful of solid utility functions. libphutil is used by Phabricator, Arcanist and Diviner. + +LICENSE + +libphutil is released under the Apache 2.0 license except as otherwise noted. diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php index 037d1a2..db73b4c 100644 --- a/scripts/__init_script__.php +++ b/scripts/__init_script__.php @@ -1,84 +1,68 @@ 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. 'xdebug.max_nesting_level' => null, // Don't limit memory, doing so just generally just prevents us from // processing large inputs without many tangible benefits. 'memory_limit' => -1, ); 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'); } // Now, load libphutil. $root = dirname(dirname(__FILE__)); require_once $root.'/src/__phutil_library_init__.php'; } __phutil_init_script__(); diff --git a/scripts/daemon/exec/exec_daemon.php b/scripts/daemon/exec/exec_daemon.php index e7dab4b..88756ab 100755 --- a/scripts/daemon/exec/exec_daemon.php +++ b/scripts/daemon/exec/exec_daemon.php @@ -1,122 +1,106 @@ #!/usr/bin/env php setTagline('daemon executor'); $args->setSynopsis(<<parse( array( array( 'name' => 'trace', 'help' => 'Enable debug tracing.', ), array( 'name' => 'trace-memory', 'help' => 'Enable debug memory tracing.', ), array( 'name' => 'log', 'param' => 'file', 'help' => 'Send output to __file__.', ), array( 'name' => 'load-phutil-library', 'param' => 'library', 'repeat' => true, 'help' => 'Load __library__.', ), array( 'name' => 'verbose', 'help' => 'Enable verbose activity logging.', ), array( 'name' => 'more', 'wildcard' => true, ), )); $trace_memory = $args->getArg('trace-memory'); $trace_mode = $args->getArg('trace') || $trace_memory; $verbose = $args->getArg('verbose'); $log = $args->getArg('log'); if ($log) { ini_set('error_log', $log); $echo_to_stderr = true; } else { $echo_to_stderr = false; } $load = $args->getArg('load-phutil-library'); $argv = $args->getArg('more'); if ($load) { foreach ($load as $library) { $library = Filesystem::resolvePath($library); phutil_load_library($library); } } PhutilErrorHandler::initialize(); function phutil_daemon_error_listener($event, $value, array $metadata) { $message = idx($metadata, 'default_message'); if ($message) { fwrite(STDERR, $message); } } if ($echo_to_stderr) { // If the caller has used "--log" to redirect the error log to a file, PHP // won't output it to stderr so the overseer can't capture it and won't // be able to send it to the web console. Install a listener which just echoes // errors to stderr, so we always get all the messages in the log and over // stdio, so they'll show up in the web console. PhutilErrorHandler::setErrorListener('phutil_daemon_error_listener'); } $daemon = array_shift($argv); if (!$daemon) { $args->printHelpAndExit(); } $daemon = newv($daemon, array($argv)); if ($trace_mode) { $daemon->setTraceMode(); } if ($trace_memory) { $daemon->setTraceMemory(); } if ($verbose) { $daemon->setVerbose(true); } $daemon->execute(); diff --git a/scripts/daemon/launch_daemon.php b/scripts/daemon/launch_daemon.php index ca925d1..3c3996a 100755 --- a/scripts/daemon/launch_daemon.php +++ b/scripts/daemon/launch_daemon.php @@ -1,23 +1,7 @@ #!/usr/bin/env php run(); diff --git a/scripts/daemon/torture/resist-death.php b/scripts/daemon/torture/resist-death.php index f1eea22..4c8245a 100755 --- a/scripts/daemon/torture/resist-death.php +++ b/scripts/daemon/torture/resist-death.php @@ -1,35 +1,19 @@ #!/usr/bin/env php setTagline('simple calculator example'); $args->setSynopsis(<<setName('add') ->setExamples('**add** __n__ ...') ->setSynopsis('Compute the sum of a list of numbers.') ->setArguments( array( array( 'name' => 'numbers', 'wildcard' => true, ), )); $mul_workflow = id(new PhutilArgumentWorkflow()) ->setName('mul') ->setExamples('**mul** __n__ ...') ->setSynopsis('Compute the product of a list of numbers.') ->setArguments( array( array( 'name' => 'numbers', 'wildcard' => true, ), )); $flow = $args->parseWorkflows( array( $add_workflow, $mul_workflow, new PhutilHelpArgumentWorkflow(), )); $nums = $args->getArg('numbers'); if (empty($nums)) { echo "You must provide one or more numbers!\n"; exit(1); } foreach ($nums as $num) { if (!is_numeric($num)) { echo "Number '{$num}' is not numeric!\n"; exit(1); } } switch ($flow->getName()) { case 'add': echo array_sum($nums)."\n"; break; case 'mul': echo array_product($nums)."\n"; break; } diff --git a/scripts/example/subworkflow.php b/scripts/example/subworkflow.php index bd3638e..8ac27a5 100755 --- a/scripts/example/subworkflow.php +++ b/scripts/example/subworkflow.php @@ -1,83 +1,67 @@ #!/usr/bin/env php setTagline('crazy workflow delgation'); $args->setSynopsis(<<getUnconsumedArgumentVector(); echo implode(' ', $unconsumed)."\n"; return 0; } } // This shows how to delegate to sub-workflows. final class PhutilArgumentWorkflowDoExample extends PhutilArgumentWorkflow { public function isExecutable() { return true; } public function shouldParsePartial() { return true; } public function execute(PhutilArgumentParser $args) { $echo_workflow = id(new PhutilArgumentWorkflowEchoExample()) ->setName('echo') ->setExamples('**echo** __string__ ...') ->setSynopsis('Echo __string__.'); $args->parseWorkflows( array( $echo_workflow, new PhutilHelpArgumentWorkflow(), )); } } $do_workflow = id(new PhutilArgumentWorkflowDoExample()) ->setName('do') ->setExamples('**do** __thing__ ...') ->setSynopsis('Do __thing__.'); $args->parseWorkflows( array( $do_workflow, new PhutilHelpArgumentWorkflow(), )); diff --git a/scripts/kill_init.php b/scripts/kill_init.php index 20364b7..1c8db6c 100755 --- a/scripts/kill_init.php +++ b/scripts/kill_init.php @@ -1,82 +1,66 @@ #!/usr/bin/env php \n"; echo "Purpose: Delete __init__.php files and move source files a level up.\n"; exit(1); } $inits = id(new FileFinder($root)) ->withType('f') ->withPath('*/__init__.php') ->find(); echo "\nDeleting ".count($inits)." __init__.php...\n"; if ($inits) { phutil_passthru('(cd %s; git rm %Ls)', $root, $inits); } $phps = id(new FileFinder($root)) ->excludePath('./docs/*') ->excludePath('*/__tests__/*') ->withType('f') ->withPath('./*/*/*') ->find(); echo "\nMoving ".count($phps)." files...\n"; foreach ($phps as $file) { echo $file."\n"; $target = dirname(dirname($file)); phutil_passthru('(cd %s; git mv %s %s)', $root, $file, $target); } $tests = id(new FileFinder($root)) ->withType('f') ->withPath('./*/*/__tests__/*.php') ->find(); echo "\nMoving ".count($tests)." tests...\n"; foreach ($tests as $file) { echo $file."\n"; $target = dirname(dirname(dirname($file))).'/__tests__/'; if (!file_exists($root.'/'.$target)) { mkdir($root.'/'.$target); } phutil_passthru('(cd %s; git mv %s %s)', $root, $file, $target); } $data = id(new FileFinder($root)) ->withType('d') ->withPath('./*/*/__tests__/data') ->find(); echo "\nMoving ".count($data)." test data dirs...\n"; foreach ($data as $dir) { echo $dir."\n"; $target = dirname(dirname($dir)); $target = dirname($target).'/__tests__/'.basename($target); phutil_passthru('(cd %s; git mv %s %s)', $root, $dir, $target); } echo "\nDone.\n"; echo "Consider running `git clean -df`.\n"; if ($data) { echo "You must manually change the path to test data dirs in test files!\n"; } diff --git a/scripts/sandpit/harden_directory.php b/scripts/sandpit/harden_directory.php index bd19179..2a7c11b 100755 --- a/scripts/sandpit/harden_directory.php +++ b/scripts/sandpit/harden_directory.php @@ -1,219 +1,203 @@ #!/usr/bin/env php $info) { $hash = $info['hash']; $type = $info['type']; $obj = $obj_root.hash_path($hash); $link = $hardened.'/'.$path; $dir = dirname($link); if (!is_dir($dir)) { $ok = mkdir($dir, 0777, $recursive = true); if (!$ok) { throw new Exception("Failed to make directory for '{$link}'!"); } } // We need to use actual symlinks in this case. if ($type == 'link') { $ok = symlink(readlink($soft.'/'.$path), $link); if (!$ok) { throw new Exception("Failed to create symlink '{$link}'!"); } continue; } if ($type == 'exec') { // Multiple hardlinks share a single executable bit, so we need to keep // executable versions separate from nonexecutable versions. $obj .= '.x'; } // Make copies of each object, obj.0, obj.1, etc., after there are too many // hardlinks. This can occur for the empty file, particularly. $n = 0; do { $stat = @lstat($obj.'.'.$n); if (!$stat) { break; } if ($stat[3] < 32000) { // TODO: On NTFS, this needs to be 1023. It is // not apparently trivial to determine if a disk // is NTFS or not, or what the link limit for a // disk is. On linux "df -T /path/to/dir" may be // useful, but on OS X this does something totally // different... break; } ++$n; } while (true); $obj = $obj.'.'.$n; if ($stat === false) { $ok = mkdir(dirname($obj), 0777, $recursive = true); if (!$ok) { throw new Exception("Failed to make directory for '{$obj}'."); } $ok = copy($soft.'/'.$path, $obj); if (!$ok) { throw new Exception("Failed to copy file '{$soft}/{$path}'!"); } if ($type == 'exec') { $ok = chmod($obj, 0755); if (!$ok) { throw new Exception("Failed to chmod file '{$obj}'!"); } } } $ok = link($obj, $link); if (!$ok) { throw new Exception("Failed to hardlink '{$obj}' to '{$link}'!"); } } // TODO: Replace link to soft directory with link to hardened directory. // execx('ln -sf %s %s', $dst, $hardened); echo $hardened."\n"; exit(0); function hash_path($hash) { return preg_replace('/([a-z0-9]{2})/', '\1/', $hash, 3); } function map_directory($dir) { try { if (Filesystem::pathExists($dir.'/.git')) { list($list) = execx( '(cd %s && git ls-tree -r --full-tree --abbrev=40 HEAD)', $dir); $list = trim($list); $list = explode("\n", $list); $map = array(); foreach ($list as $line) { $matches = null; $regexp = '/^(\d{6}) (\w+) ([a-z0-9]{40})\t(.*)$/'; if (!preg_match($regexp, $line, $matches)) { throw new Exception("Unable to parse line '{$line}'!"); } $flag = $matches[1]; $type = $matches[2]; $hash = $matches[3]; $file = $matches[4]; if ($type == 'commit') { // Deal with Git submodules. $submap = map_directory($dir.'/'.$file); foreach ($submap as $subfile => $info) { $map[$file.'/'.$subfile] = $info; } } else { $mask = (int)base_convert($flag, 8, 10); $type = 'file'; if ($mask & 0111) { echo "EXEC: {$file}\n"; $type = 'exec'; } else if (($mask & 0120000) == 0120000) { $type = 'link'; } $map[$file] = array( 'hash' => $hash, 'type' => $type, ); } } return $map; } } catch (Exception $ex) { phlog($ex); // Just drop down and go with the non-git approach. } $files = id(new FileFinder($dir)) ->withType('f') ->excludePath('*/.git/*') ->excludePath('*/.svn/*') ->find(); foreach ($files as $file) { if (!strncmp($file, './', 2)) { $file = substr($file, 2); } $data = Filesystem::readFile($dir.'/'.$file); $len = strlen($data); $hash = sha1("blob {$len}\0{$data}"); $type = 'file'; if (is_link($dir.'/'.$file)) { $type = 'link'; } else if (is_executable($dir.'/'.$file)) { $type = 'exec'; } $map[$file] = array( 'hash' => $hash, 'type' => $type, ); } return $map; } diff --git a/scripts/test/deferred_log.php b/scripts/test/deferred_log.php index 5611336..58a5b29 100755 --- a/scripts/test/deferred_log.php +++ b/scripts/test/deferred_log.php @@ -1,28 +1,12 @@ #!/usr/bin/env php getProtocol() == 'https') { $future = new HTTPSFuture($uri, $data); } else { $future = new HTTPFuture($uri, $data); } $future->setMethod($method); $future->setTimeout($timeout); print_r($future->resolve()); diff --git a/scripts/test/interactive_editor.php b/scripts/test/interactive_editor.php index 422f395..2bd8ae3 100755 --- a/scripts/test/interactive_editor.php +++ b/scripts/test/interactive_editor.php @@ -1,78 +1,62 @@ #!/usr/bin/env php setTagline('test InteractiveEditor class'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'fallback', 'param' => 'editor', 'help' => 'Set the fallback editor.', ), array( 'name' => 'line', 'short' => 'l', 'param' => 'number', 'help' => 'Open at line number __number__.', ), array( 'name' => 'name', 'param' => 'filename', 'help' => 'Set edited file name.', ), )); if ($args->getArg('help')) { $args->printHelpAndExit(); } $editor = new PhutilInteractiveEditor( "The wizard quickly\n". "jinxed the gnomes\n". "before they vaporized."); $name = $args->getArg('name'); if ($name) { $editor->setName($name); } $line = $args->getArg('line'); if ($line) { $editor->setLineOffset($line); } $fallback = $args->getArg('fallback'); if ($fallback) { $editor->setFallbackEditor($fallback); } $result = $editor->editInteractively(); echo "Edited Text:\n{$result}\n"; diff --git a/scripts/test/mime.php b/scripts/test/mime.php index bfc4bf6..872d50f 100755 --- a/scripts/test/mime.php +++ b/scripts/test/mime.php @@ -1,56 +1,40 @@ #!/usr/bin/env php setTagline('test Filesystem::getMimeType()'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'default', 'param' => 'mimetype', 'help' => 'Use __mimetype__ as default instead of builtin default.', ), array( 'name' => 'file', 'wildcard' => true, ), )); $file = $args->getArg('file'); if (count($file) !== 1) { $args->printHelpAndExit(); } $file = reset($file); $default = $args->getArg('default'); if ($default) { echo Filesystem::getMimeType($file, $default)."\n"; } else { echo Filesystem::getMimeType($file)."\n"; } diff --git a/scripts/test/prompt.php b/scripts/test/prompt.php index 51552ee..f17563f 100755 --- a/scripts/test/prompt.php +++ b/scripts/test/prompt.php @@ -1,52 +1,36 @@ #!/usr/bin/env php setTagline('test console prompting'); $args->setSynopsis(<<parseStandardArguments(); $args->parse( array( array( 'name' => 'history', 'param' => 'file', 'default' => '', 'help' => 'Use specified history __file__.', ), array( 'name' => 'prompt', 'param' => 'text', 'default' => 'Enter some text:', 'help' => 'Change the prompt text to __text__.' ), )); $result = phutil_console_prompt( $args->getArg('prompt'), $args->getArg('history')); $console = PhutilConsole::getConsole(); $console->writeOut("Input is: %s\n", $result); diff --git a/scripts/test/test_service_profiler.php b/scripts/test/test_service_profiler.php index 41c2b99..c8eee8e 100755 --- a/scripts/test/test_service_profiler.php +++ b/scripts/test/test_service_profiler.php @@ -1,34 +1,18 @@ #!/usr/bin/env php getServiceCallLog()); diff --git a/scripts/utils/directory_fixture.php b/scripts/utils/directory_fixture.php index 794d2a0..26ff7e4 100755 --- a/scripts/utils/directory_fixture.php +++ b/scripts/utils/directory_fixture.php @@ -1,77 +1,61 @@ #!/usr/bin/env php setTagline('edit directory fixtures'); $args->setSynopsis(<<parseStandardArguments(); $args->parse(array( array( 'name' => 'create', 'help' => 'Create a new fixture.', ), array( 'name' => 'files', 'wildcard' => true, ), )); $is_create = $args->getArg('create'); $files = $args->getArg('files'); if (count($files) !== 1) { echo "Specify exactly one file to edit.\n"; exit(1); } $file = head($files); if ($is_create) { if (Filesystem::pathExists($file)) { echo "File '{$file}' already exists!\n"; exit(1); } $fixture = PhutilDirectoryFixture::newEmptyFixture(); } else { if (!Filesystem::pathExists($file)) { echo "File '{$file}' does not exist!\n"; exit(1); } $fixture = PhutilDirectoryFixture::newFromArchive($file); } echo "Spawning an interactive shell. Exit when complete.\n\n"; $err = phutil_passthru('cd %s && sh', $fixture->getPath()); if ($err) { exit($err); } echo "Updating archive...\n"; $fixture->saveToArchive($file); echo "Done.\n"; diff --git a/scripts/utils/envinfo.php b/scripts/utils/envinfo.php index d55816e..02bf0b6 100644 --- a/scripts/utils/envinfo.php +++ b/scripts/utils/envinfo.php @@ -1,43 +1,27 @@ #!/usr/bin/env php setTagline('acquire and hold a lockfile'); $args->setSynopsis(<<parseStandardArguments(); $args->parse(array( array( 'name' => 'test', 'help' => 'Instead of holding the lock, release it and exit.', ), array( 'name' => 'hold', 'help' => 'Hold indefinitely without prompting.', ), array( 'name' => 'wait', 'param' => 'n', 'help' => 'Block for up to __n__ seconds waiting for the lock.', 'default' => 0, ), array( 'name' => 'file', 'wildcard' => true, ), )); $file = $args->getArg('file'); if (count($file) != 1) { $args->printHelpAndExit(); } $file = head($file); $console = PhutilConsole::getConsole(); $console->writeOut("This process has PID %d. Acquiring lock...\n", getmypid()); $lock = PhutilFileLock::newForPath($file); try { $lock->lock($args->getArg('wait')); } catch (PhutilFileLockException $ex) { $console->writeOut("**UNABLE TO ACQUIRE LOCK:** Lock is already held.\n"); exit(1); } // NOTE: This string is magic, the unit tests look for it. $console->writeOut("LOCK ACQUIRED\n"); if ($args->getArg('test')) { $lock->unlock(); exit(0); } if ($args->getArg('hold')) { while (true) { sleep(1); } } while (!$console->confirm("Release lock?")) { // Keep asking until they say yes. } $console->writeOut("Unlocking...\n"); $lock->unlock(); $console->writeOut("Done.\n"); exit(0); diff --git a/scripts/utils/utf8.php b/scripts/utils/utf8.php index 0b2ecda..3775dc8 100755 --- a/scripts/utils/utf8.php +++ b/scripts/utils/utf8.php @@ -1,185 +1,169 @@ #!/usr/bin/env php setTagline('utf8 charset test script'); $args->setSynopsis(<<parseStandardArguments(); $args->parse(array( array( 'name' => 'context', 'short' => 'C', 'param' => 'lines', 'default' => 3, 'help' => 'Show __lines__ lines of context instead of the default 3.', 'conflicts' => array( 'test' => 'with --test, context is not shown.', ), ), array( 'name' => 'test', 'short' => 't', 'help' => 'Print file names containing invalid UTF-8 to stdout.', ), array( 'name' => 'files', 'wildcard' => true, ), )); $is_test = $args->getArg('test'); $context = $args->getArg('context'); $files = $args->getArg('files'); if (empty($files)) { $args->printHelpAndExit(); } if ($is_test) { $err = test($files); } else { $err = show($files, $context); } exit($err); function read($file) { if ($file == '-') { return file_get_contents('php://stdin'); } else { return Filesystem::readFile($file); } } function name($file) { if ($file == '-') { return 'stdin'; } else { return $file; } } function test(array $files) { foreach ($files as $file) { $data = read($file); if (!phutil_is_utf8($data)) { echo name($file)."\n"; } } return 0; } function show(array $files, $context) { foreach ($files as $file) { $data = read($file); $ok = phutil_is_utf8($data); if ($ok) { echo "OKAY"; } else { echo "FAIL"; } echo " ".name($file)."\n"; if (!$ok) { $lines = explode("\n", $data); $len = count($lines); $map = array(); $bad = array(); foreach ($lines as $n => $line) { if (phutil_is_utf8($line)) { continue; } $bad[$n] = true; for ($jj = max(0, $n - $context); $jj < min($len, $n + 1 + $context); $jj++) { $map[$jj] = true; } } $width = strlen(max(array_keys($map))); // Set $last such that we print a newline on the first iteration thorugh // the loop. $last = -2; foreach ($map as $idx => $ignored) { if ($idx != $last + 1) { printf("\n"); } $last = $idx; $line = $lines[$idx]; if (!empty($bad[$idx])) { $line = show_problems($line); } printf(" % {$width}d %s\n", $idx + 1, $line); } echo "\n"; } } return 0; } function show_problems($line) { $regex = "/^(". "[\x01-\x7F]+". "|([\xC2-\xDF][\x80-\xBF])". "|([\xE0-\xEF][\x80-\xBF][\x80-\xBF])". "|([\xF0-\xF4][\x80-\xBF][\x80-\xBF][\x80-\xBF]))/"; $out = ''; while (strlen($line)) { $match = null; if (preg_match($regex, $line, $match)) { $out .= $match[1]; $line = substr($line, strlen($match[1])); } else { $chr = sprintf("<0x%0X>", ord($line[0])); $chr = phutil_console_format('##%s##', $chr); $out .= $chr; $line = substr($line, 1); } } return $out; } diff --git a/src/__phutil_library_init__.php b/src/__phutil_library_init__.php index 0123715..63c01e2 100644 --- a/src/__phutil_library_init__.php +++ b/src/__phutil_library_init__.php @@ -1,429 +1,413 @@ loadModule($library, $module); } /** * @group library */ function phutil_require_source($source) { PhutilBootloader::getInstance()->loadSource($source); } /** * @group library */ function phutil_register_library($library, $path) { PhutilBootloader::getInstance()->registerLibrary($library, $path); } /** * @group library */ function phutil_register_library_map(array $map) { PhutilBootloader::getInstance()->registerLibraryMap($map); } /** * @group library */ function phutil_load_library($path) { PhutilBootloader::getInstance()->loadLibrary($path); } /** * @group library */ function phutil_is_windows() { // We can also use PHP_OS, but that's kind of sketchy because it returns // "WINNT" for Windows 7 and "Darwin" for Mac OS X. Practically, testing for // DIRECTORY_SEPARATOR is more straightforward. return (DIRECTORY_SEPARATOR != '/'); } /** * @group library */ function phutil_is_hiphop_runtime() { return (array_key_exists('HPHP', $_ENV) && $_ENV['HPHP'] === 1); } /** * @group library */ final class PhutilBootloader { private static $instance; private $registeredLibraries = array(); private $libraryMaps = array(); private $moduleStack = array(); private $currentLibrary = null; private $classTree = array(); public static function getInstance() { if (!self::$instance) { self::$instance = new PhutilBootloader(); } return self::$instance; } private function __construct() { // This method intentionally left blank. } public function getClassTree() { return $this->classTree; } public function registerLibrary($name, $path) { if (basename($path) != '__phutil_library_init__.php') { throw new PhutilBootloaderException( 'Only directories with a __phutil_library_init__.php file may be '. 'registered as libphutil libraries.'); } $path = dirname($path); // Detect attempts to load the same library multiple times from different // locations. This might mean you're doing something silly like trying to // include two different versions of something, or it might mean you're // doing something subtle like running a different version of 'arc' on a // working copy of Arcanist. if (isset($this->registeredLibraries[$name])) { $old_path = $this->registeredLibraries[$name]; if ($old_path != $path) { throw new PhutilLibraryConflictException($name, $old_path, $path); } } $this->registeredLibraries[$name] = $path; // TODO: Remove this once we drop libphutil v1 support. $version = $this->getLibraryFormatVersion($name); if ($version == 1) { return $this; } // For libphutil v2 libraries, load all functions when we load the library. if (!class_exists('PhutilSymbolLoader', false)) { $root = $this->getLibraryRoot('phutil'); $this->executeInclude($root.'/symbols/PhutilSymbolLoader.php'); } $loader = new PhutilSymbolLoader(); $loader ->setLibrary($name) ->setType('function'); try { $loader->selectAndLoadSymbols(); } catch (PhutilMissingSymbolException $ex) { // Ignore this, it happens if a global function is removed. Everything // else loaded so proceed forward: worst case is a fatal when we // hit a function call to a function which no longer exists, which is // no worse than fataling here. } return $this; } public function registerLibraryMap(array $map) { $this->libraryMaps[$this->currentLibrary] = $map; return $this; } public function getLibraryMap($name) { if (empty($this->libraryMaps[$name])) { $root = $this->getLibraryRoot($name); $this->currentLibrary = $name; $okay = include $root.'/__phutil_library_map__.php'; if (!$okay) { throw new PhutilBootloaderException( "Include of '{$root}/__phutil_library_map__.php' failed!"); } $map = $this->libraryMaps[$name]; // NOTE: We can't use "idx()" here because it may not be loaded yet. $version = isset($map['__library_version__']) ? $map['__library_version__'] : 1; switch ($version) { case 1: // NOTE: In the original version of the library, the map stored // separate 'requires_class' (always a string) and // 'requires_interface' keys (always an array). Load them into the // classtree. // TODO: Remove support once we drop libphutil v1 support. foreach ($map['requires_class'] as $child => $parent) { $this->classTree[$parent][] = $child; } foreach ($map['requires_interface'] as $child => $parents) { foreach ($parents as $parent) { $this->classTree[$parent][] = $child; } } break; case 2: // NOTE: In version 2 of the library format, all parents (both // classes and interfaces) are stored in the 'xmap'. The value is // either a string for a single parent (the common case) or an array // for multiple parents. foreach ($map['xmap'] as $child => $parents) { foreach ((array)$parents as $parent) { $this->classTree[$parent][] = $child; } } break; default: throw new Exception("Unsupported library version '{$version}'!"); } } return $this->libraryMaps[$name]; } public function getLibraryFormatVersion($name) { $map = $this->getLibraryMap($name); // NOTE: We can't use "idx()" here because it may not be loaded yet. $version = isset($map['__library_version__']) ? $map['__library_version__'] : 1; return $version; } public function getLibraryRoot($name) { if (empty($this->registeredLibraries[$name])) { throw new PhutilBootloaderException( "The phutil library '{$name}' has not been loaded!"); } return $this->registeredLibraries[$name]; } public function getAllLibraries() { return array_keys($this->registeredLibraries); } private function pushModuleStack($library, $module) { array_push($this->moduleStack, $this->getLibraryRoot($library).'/'.$module); return $this; } private function popModuleStack() { array_pop($this->moduleStack); } private function peekModuleStack() { return end($this->moduleStack); } public function loadLibrary($path) { $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { if ($path[0] != '/') { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } } $okay = $this->executeInclude($root.$path.'/__phutil_library_init__.php'); if (!$okay) { throw new PhutilBootloaderException( "Include of '{$path}/__phutil_library_init__.php' failed!"); } } public function loadModule($library, $module) { $version = $this->getLibraryFormatVersion($library); if ($version == 2) { // If a v1 library has a "phutil_require_module(...)" for a v2 library, // ignore it. We load functions on library registration and autoload // classes. return; } $this->pushModuleStack($library, $module); phutil_require_source('__init__.php'); $this->popModuleStack(); } public function loadLibrarySource($library, $source) { $path = $this->getLibraryRoot($library).'/'.$source; $okay = $this->executeInclude($path); if (!$okay) { throw new PhutilBootloaderException("Include of '{$path}' failed!"); } } public function loadSource($source) { $base = $this->peekModuleStack(); $okay = $this->executeInclude($base.'/'.$source); if (!$okay) { throw new PhutilBootloaderException( "Include of '{$base}/{$source}' failed!"); } } public function moduleExists($library, $module) { $path = $this->getLibraryRoot($library); return @file_exists($path.'/'.$module.'/__init__.php'); } private function executeInclude($path) { // Suppress warning spew if the file does not exist; we'll throw an // exception instead. We still emit error text in the case of syntax errors. $old = error_reporting(E_ALL & ~E_WARNING); $okay = include_once $path; error_reporting($old); return $okay; } } /** * @group library */ final class PhutilBootloaderException extends Exception { } /** * Thrown when you attempt to load two different copies of a library with the * same name. Trying to load the second copy of the library will trigger this, * and the library will not be loaded. * * This means you've either done something silly (like tried to explicitly load * two different versions of the same library into the same program -- this * won't work because they'll have namespace conflicts), or your configuration * might have some problems which caused two parts of your program to try to * load the same library but end up loading different copies of it, or there * may be some subtle issue like running 'arc' in a different Arcanist working * directory. (Some bootstrapping workflows like that which run low-level * library components on other copies of themselves are expected to fail.) * * To resolve this, you need to make sure your program loads no more than one * copy of each libphutil library, but exactly how you approach this depends on * why it's happening in the first place. * * @task info Getting Exception Information * @task construct Creating Library Conflict Exceptions * @group library */ final class PhutilLibraryConflictException extends Exception { private $library; private $oldPath; private $newPath; /** * Create a new library conflict exception. * * @param string The name of the library which conflicts with an existing * library. * @param string The path of the already-loaded library. * @param string The path of the attempting-to-load library. * * @task construct */ public function __construct($library, $old_path, $new_path) { $this->library = $library; $this->oldPath = $old_path; $this->newPath = $new_path; $message = "Library conflict! The library '{$library}' has already been ". "loaded (from '{$old_path}') but is now being loaded again ". "from a new location ('{$new_path}'). You can not load ". "multiple copies of the same library into a program."; parent::__construct($message); } /** * Retrieve the name of the library in conflict. * * @return string The name of the library which conflicts with an existing * library. * @task info */ public function getLibrary() { return $this->library; } /** * Get the path to the library which has already been loaded earlier in the * program's execution. * * @return string The path of the already-loaded library. * @task info */ public function getOldPath() { return $this->oldPath; } /** * Get the path to the library which is causing this conflict. * * @return string The path of the attempting-to-load library. * @task info */ public function getNewPath() { return $this->newPath; } } /** * @group library */ function __phutil_autoload($class_name) { try { $loader = new PhutilSymbolLoader(); $symbols = $loader ->setType('class') ->setName($class_name) ->selectAndLoadSymbols(); if (!$symbols) { throw new PhutilMissingSymbolException($class_name); } } catch (PhutilMissingSymbolException $ex) { // If there are other SPL autoloaders installed, we need to give them a // chance to load the class. Throw the exception if we're the last // autoloader; if not, swallow it and let them take a shot. $autoloaders = spl_autoload_functions(); $last = end($autoloaders); if ($last == '__phutil_autoload') { throw $ex; } } } spl_autoload_register('__phutil_autoload', $throw = true); phutil_register_library('phutil', __FILE__); diff --git a/src/aphront/storage/connection/AphrontDatabaseConnection.php b/src/aphront/storage/connection/AphrontDatabaseConnection.php index 84b130a..0db84e9 100644 --- a/src/aphront/storage/connection/AphrontDatabaseConnection.php +++ b/src/aphront/storage/connection/AphrontDatabaseConnection.php @@ -1,196 +1,180 @@ getTransactionState(); $point = $state->getSavepointName(); $depth = $state->getDepth(); $new_transaction = ($depth == 0); if ($new_transaction) { $this->query('START TRANSACTION'); } else { $this->query('SAVEPOINT '.$point); } $state->increaseDepth(); return $this; } /** * Commit a transaction, or stage a savepoint for commit once the entire * transaction completes if inside a transaction stack. * * @return this * @task xaction */ public function saveTransaction() { $state = $this->getTransactionState(); $depth = $state->decreaseDepth(); if ($depth == 0) { $this->query('COMMIT'); } return $this; } /** * Rollback a transaction, or unstage the last savepoint if inside a * transaction stack. * * @return this */ public function killTransaction() { $state = $this->getTransactionState(); $depth = $state->decreaseDepth(); if ($depth == 0) { $this->query('ROLLBACK'); } else { $this->query('ROLLBACK TO SAVEPOINT '.$state->getSavepointName()); } return $this; } /** * Returns true if the connection is transactional. * * @return bool True if the connection is currently transactional. * @task xaction */ public function isInsideTransaction() { $state = $this->getTransactionState(); return ($state->getDepth() > 0); } /** * Get the current @{class:AphrontDatabaseTransactionState} object, or create * one if none exists. * * @return AphrontDatabaseTransactionState Current transaction state. * @task xaction */ protected function getTransactionState() { if (!$this->transactionState) { $this->transactionState = new AphrontDatabaseTransactionState(); } return $this->transactionState; } /** * @task xaction */ public function beginReadLocking() { $this->getTransactionState()->beginReadLocking(); return $this; } /** * @task xaction */ public function endReadLocking() { $this->getTransactionState()->endReadLocking(); return $this; } /** * @task xaction */ public function isReadLocking() { return $this->getTransactionState()->isReadLocking(); } /** * @task xaction */ public function beginWriteLocking() { $this->getTransactionState()->beginWriteLocking(); return $this; } /** * @task xaction */ public function endWriteLocking() { $this->getTransactionState()->endWriteLocking(); return $this; } /** * @task xaction */ public function isWriteLocking() { return $this->getTransactionState()->isWriteLocking(); } } diff --git a/src/aphront/storage/connection/AphrontDatabaseTransactionState.php b/src/aphront/storage/connection/AphrontDatabaseTransactionState.php index 13f1a8a..f19bc7f 100644 --- a/src/aphront/storage/connection/AphrontDatabaseTransactionState.php +++ b/src/aphront/storage/connection/AphrontDatabaseTransactionState.php @@ -1,104 +1,88 @@ depth; } public function increaseDepth() { return ++$this->depth; } public function decreaseDepth() { if ($this->depth == 0) { throw new Exception( 'Too many calls to saveTransaction() or killTransaction()!'); } return --$this->depth; } public function getSavepointName() { return 'Aphront_Savepoint_'.$this->depth; } public function beginReadLocking() { $this->readLockLevel++; return $this; } public function endReadLocking() { if ($this->readLockLevel == 0) { throw new Exception("Too many calls to endReadLocking()!"); } $this->readLockLevel--; return $this; } public function isReadLocking() { return ($this->readLockLevel > 0); } public function beginWriteLocking() { $this->writeLockLevel++; return $this; } public function endWriteLocking() { if ($this->writeLockLevel == 0) { throw new Exception("Too many calls to endWriteLocking()!"); } $this->writeLockLevel--; return $this; } public function isWriteLocking() { return ($this->writeLockLevel > 0); } public function __destruct() { if ($this->depth) { throw new Exception( 'Process exited with an open transaction! The transaction will be '. 'implicitly rolled back. Calls to openTransaction() must always be '. 'paired with a call to saveTransaction() or killTransaction().'); } if ($this->readLockLevel) { throw new Exception( 'Process exited with an open read lock! Call to beginReadLocking() '. 'must always be paired with a call to endReadLocking().'); } if ($this->writeLockLevel) { throw new Exception( 'Process exited with an open write lock! Call to beginWriteLocking() '. 'must always be paired with a call to endWriteLocking().'); } } } diff --git a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php b/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php index e83ec5c..f02dcae 100644 --- a/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php +++ b/src/aphront/storage/connection/AphrontIsolatedDatabaseConnection.php @@ -1,126 +1,110 @@ configuration = $configuration; if (self::$nextInsertID === null) { // Generate test IDs into a distant ID space to reduce the risk of // collisions and make them distinctive. self::$nextInsertID = 55555000000 + mt_rand(0, 1000); } } public function close() { return; } public function escapeString($string) { return ''; } public function escapeColumnName($name) { return ''; } public function escapeMultilineComment($comment) { return ''; } public function escapeStringForLikeClause($value) { return ''; } private function getConfiguration($key, $default = null) { return idx($this->configuration, $key, $default); } public function getInsertID() { return $this->insertID; } public function getAffectedRows() { return $this->affectedRows; } public function selectAllResults() { return $this->allResults; } public function executeRawQuery($raw_query) { // NOTE: "[\s<>K]*" allows any number of (properly escaped) comments to // appear prior to the allowed keyword, since this connection escapes // them as "" (above). $keywords = array( 'INSERT', 'UPDATE', 'DELETE', 'START', 'SAVEPOINT', 'COMMIT', 'ROLLBACK', ); $preg_keywords = array(); foreach ($keywords as $key => $word) { $preg_keywords[] = preg_quote($word, '/'); } $preg_keywords = implode('|', $preg_keywords); if (!preg_match('/^[\s<>K]*('.$preg_keywords.')\s*/i', $raw_query)) { throw new AphrontQueryNotSupportedException( "Database isolation currently only supports some queries. You are ". "trying to issue a query which does not begin with an allowed ". "keyword (".implode(', ', $keywords)."): '".$raw_query."'"); } $this->transcript[] = $raw_query; // NOTE: This method is intentionally simplified for now, since we're only // using it to stub out inserts/updates. In the future it will probably need // to grow more powerful. $this->allResults = array(); // NOTE: We jitter the insert IDs to keep tests honest; a test should cover // the relationship between objects, not their exact insertion order. This // guarantees that IDs are unique but makes it impossible to hard-code tests // against this specific implementation detail. self::$nextInsertID += mt_rand(1, 10); $this->insertID = self::$nextInsertID; $this->affectedRows = 1; } public function getQueryTranscript() { return $this->transcript; } } diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php index 2a61c08..fdbbba5 100644 --- a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php +++ b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php @@ -1,104 +1,88 @@ requireConnection()); } public function getInsertID() { return mysql_insert_id($this->requireConnection()); } public function getAffectedRows() { return mysql_affected_rows($this->requireConnection()); } protected function closeConnection() { mysql_close($this->requireConnection()); } protected function connect() { if (!function_exists('mysql_connect')) { // We have to '@' the actual call since it can spew all sorts of silly // noise, but it will also silence fatals caused by not having MySQL // installed, which has bitten me on three separate occasions. Make sure // such failures are explicit and loud. throw new Exception( "About to call mysql_connect(), but the PHP MySQL extension is not ". "available!"); } $user = $this->getConfiguration('user'); $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); $pass = $this->getConfiguration('pass'); if ($pass instanceof PhutilOpaqueEnvelope) { $pass = $pass->openEnvelope(); } $conn = @mysql_connect( $host, $user, $pass, $new_link = true, $flags = 0); if (!$conn) { $errno = mysql_errno(); $error = mysql_error(); throw new AphrontQueryConnectionException( "Attempt to connect to {$user}@{$host} failed with error ". "#{$errno}: {$error}.", $errno); } if ($database !== null) { $ret = @mysql_select_db($database, $conn); if (!$ret) { $this->throwQueryException($conn); } } mysql_set_charset('utf8', $conn); return $conn; } protected function rawQuery($raw_query) { return @mysql_query($raw_query, $this->requireConnection()); } protected function fetchAssoc($result) { return mysql_fetch_assoc($result); } protected function getErrorCode($connection) { return mysql_errno($connection); } protected function getErrorDescription($connection) { return mysql_error($connection); } } diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php index a915994..e148a6f 100644 --- a/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php +++ b/src/aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php @@ -1,265 +1,249 @@ configuration = $configuration; } public function close() { if ($this->lastResult) { $this->lastResult = null; } if ($this->connection) { $this->closeConnection(); $this->connection = null; } } public function escapeColumnName($name) { return '`'.str_replace('`', '``', $name).'`'; } public function escapeMultilineComment($comment) { // These can either terminate a comment, confuse the hell out of the parser, // make MySQL execute the comment as a query, or, in the case of semicolon, // are quasi-dangerous because the semicolon could turn a broken query into // a working query plus an ignored query. static $map = array( '--' => '(DOUBLEDASH)', '*/' => '(STARSLASH)', '//' => '(SLASHSLASH)', '#' => '(HASH)', '!' => '(BANG)', ';' => '(SEMICOLON)', ); $comment = str_replace( array_keys($map), array_values($map), $comment); // For good measure, kill anything else that isn't a nice printable // character. $comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment); return '/* '.$comment.' */'; } public function escapeStringForLikeClause($value) { $value = addcslashes($value, '\%_'); $value = $this->escapeString($value); return $value; } protected function getConfiguration($key, $default = null) { return idx($this->configuration, $key, $default); } private function establishConnection() { $start = microtime(true); $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'connect', 'host' => $host, 'database' => $database, )); $retries = max(1, $this->getConfiguration('retries', 3)); while ($retries--) { try { $conn = $this->connect(); $profiler->endServiceCall($call_id, array()); break; } catch (AphrontQueryException $ex) { if ($retries && $ex->getCode() == 2003) { $class = get_class($ex); $message = $ex->getMessage(); phlog("Retrying ({$retries}) after {$class}: {$message}"); } else { $profiler->endServiceCall($call_id, array()); throw $ex; } } } $this->connection = $conn; } protected function requireConnection() { if (!$this->connection) { $this->establishConnection(); } return $this->connection; } public function selectAllResults() { $result = array(); $res = $this->lastResult; if ($res == null) { throw new Exception('No query result to fetch from!'); } while (($row = $this->fetchAssoc($res))) { $result[] = $row; } return $result; } public function executeRawQuery($raw_query) { $this->lastResult = null; $retries = max(1, $this->getConfiguration('retries', 3)); while ($retries--) { try { $this->requireConnection(); // TODO: Do we need to include transactional statements here? $is_write = !preg_match('/^(SELECT|SHOW|EXPLAIN)\s/', $raw_query); if ($is_write) { AphrontWriteGuard::willWrite(); } $start = microtime(true); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'query', 'config' => $this->configuration, 'query' => $raw_query, 'write' => $is_write, )); $result = $this->rawQuery($raw_query); $profiler->endServiceCall($call_id, array()); if ($this->nextError) { $result = null; } if ($result) { $this->lastResult = $result; break; } $this->throwQueryException($this->connection); } catch (AphrontQueryConnectionLostException $ex) { if ($this->isInsideTransaction()) { // Zero out the transaction state to prevent a second exception // ("program exited with open transaction") from being thrown, since // we're about to throw a more relevant/useful one instead. $state = $this->getTransactionState(); while ($state->getDepth()) { $state->decreaseDepth(); } // We can't close the connection before this because // isInsideTransaction() and getTransactionState() depend on the // connection. $this->close(); throw $ex; } $this->close(); if (!$retries) { throw $ex; } } } } protected function throwQueryException($connection) { if ($this->nextError) { $errno = $this->nextError; $error = 'Simulated error.'; $this->nextError = null; } else { $errno = $this->getErrorCode($connection); $error = $this->getErrorDescription($connection); } $exmsg = "#{$errno}: {$error}"; switch ($errno) { case 2013: // Connection Dropped throw new AphrontQueryConnectionLostException($exmsg); case 2006: // Gone Away $more = "This error may occur if your MySQL 'wait_timeout' ". "or 'max_allowed_packet' configuration values are set too low."; throw new AphrontQueryConnectionLostException("{$exmsg}\n\n{$more}"); case 1213: // Deadlock case 1205: // Lock wait timeout exceeded throw new AphrontQueryDeadlockException($exmsg); case 1062: // Duplicate Key // NOTE: In some versions of MySQL we get a key name back here, but // older versions just give us a key index ("key 2") so it's not // portable to parse the key out of the error and attach it to the // exception. throw new AphrontQueryDuplicateKeyException($exmsg); case 1044: // Access denied to database case 1045: // Access denied (auth) case 1142: // Access denied to table case 1143: // Access denied to column throw new AphrontQueryAccessDeniedException($exmsg); case 1146: // No such table case 1049: // No such database case 1054: // Unknown column "..." in field list throw new AphrontQuerySchemaException($exmsg); default: // TODO: 1064 is syntax error, and quite terrible in production. throw new AphrontQueryException($exmsg); } } /** * Force the next query to fail with a simulated error. This should be used * ONLY for unit tests. */ public function simulateErrorOnNextQuery($error) { $this->nextError = $error; return $this; } } diff --git a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php index 0e3fac2..d477142 100644 --- a/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php +++ b/src/aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php @@ -1,94 +1,78 @@ requireConnection()->escape_string($string); } public function getInsertID() { return $this->requireConnection()->insert_id; } public function getAffectedRows() { return $this->requireConnection()->affected_rows; } protected function closeConnection() { $this->requireConnection()->close(); } protected function connect() { if (!class_exists('mysqli', false)) { throw new Exception( "About to call new mysqli(), but the PHP MySQLi extension is not ". "available!"); } $user = $this->getConfiguration('user'); $host = $this->getConfiguration('host'); $database = $this->getConfiguration('database'); $pass = $this->getConfiguration('pass'); if ($pass instanceof PhutilOpaqueEnvelope) { $pass = $pass->openEnvelope(); } $conn = @new mysqli( $host, $user, $pass, $database); $errno = $conn->connect_errno; if ($errno) { $error = $conn->connect_error; throw new AphrontQueryConnectionException( "Attempt to connect to {$user}@{$host} failed with error ". "#{$errno}: {$error}.", $errno); } $conn->set_charset('utf8'); return $conn; } protected function rawQuery($raw_query) { return @$this->requireConnection()->query($raw_query); } protected function fetchAssoc($result) { return $result->fetch_assoc(); } protected function getErrorCode($connection) { return $connection->errno; } protected function getErrorDescription($connection) { return $connection->error; } } diff --git a/src/aphront/storage/exception/AphrontQueryAccessDeniedException.php b/src/aphront/storage/exception/AphrontQueryAccessDeniedException.php index 1c2793d..b92d0a1 100644 --- a/src/aphront/storage/exception/AphrontQueryAccessDeniedException.php +++ b/src/aphront/storage/exception/AphrontQueryAccessDeniedException.php @@ -1,23 +1,7 @@ query = $query; } public function getQuery() { return $this->query; } } diff --git a/src/aphront/storage/exception/AphrontQueryRecoverableException.php b/src/aphront/storage/exception/AphrontQueryRecoverableException.php index 96c544c..ce05afd 100644 --- a/src/aphront/storage/exception/AphrontQueryRecoverableException.php +++ b/src/aphront/storage/exception/AphrontQueryRecoverableException.php @@ -1,25 +1,9 @@ dispose(); * * Normally, you do not need to manage guards yourself -- the Aphront stack * handles it for you. * * This class accepts a callback, which will be invoked when a write is * attempted. The callback should validate the presence of a CSRF token in * the request, or abort the request (e.g., by throwing an exception) if a * valid token isn't present. * * @param callable CSRF callback. * @return this * @task manage */ public function __construct($callback) { if (self::$instance) { throw new Exception( "An AphrontWriteGuard already exists. Dispose of the previous guard ". "before creating a new one."); } if (self::$allowUnguardedWrites) { throw new Exception( "An AphrontWriteGuard is being created in a context which permits ". "unguarded writes unconditionally. This is not allowed and indicates ". "a serious error."); } $this->callback = $callback; self::$instance = $this; } /** * Dispose of the active write guard. You must call this method when you are * done with a write guard. You do not normally need to call this yourself. * * @return void * @task manage */ public function dispose() { if ($this->allowDepth > 0) { throw new Exception( "Imbalanced AphrontWriteGuard: more beginUnguardedWrites() calls than ". "endUnguardedWrites() calls."); } self::$instance = null; } /** * Determine if there is an active write guard. * * @return bool * @task manage */ public static function isGuardActive() { return (bool)self::$instance; } /* -( Protecting Writes )-------------------------------------------------- */ /** * Declare intention to perform a write, validating that writes are allowed. * You should call this method before executing a write whenever you implement * a new storage engine where information can be permanently kept. * * Writes are permitted if: * * - The request has valid CSRF tokens. * - Unguarded writes have been temporarily enabled by a call to * @{method:beginUnguardedWrites}. * - All write guarding has been disabled with * @{method:allowDangerousUnguardedWrites}. * * If none of these conditions are true, this method will throw and prevent * the write. * * @return void * @task protect */ public static function willWrite() { if (!self::$instance) { if (!self::$allowUnguardedWrites) { throw new Exception( "Unguarded write! There must be an active AphrontWriteGuard to ". "perform writes."); } else { // Unguarded writes are being allowed unconditionally. return; } } $instance = self::$instance; if ($instance->allowDepth == 0) { call_user_func($instance->callback); } } /* -( Disabling Write Protection )----------------------------------------- */ /** * Enter a scope which permits unguarded writes. This works like * @{method:beginUnguardedWrites} but returns an object which will end * the unguarded write scope when its __destruct() method is called. This * is useful to more easily handle exceptions correctly in unguarded write * blocks: * * // Restores the guard even if do_logging() throws. * function unguarded_scope() { * $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); * do_logging(); * } * * @return AphrontScopedUnguardedWriteCapability Object which ends unguarded * writes when it leaves scope. * @task disable */ public static function beginScopedUnguardedWrites() { self::beginUnguardedWrites(); return new AphrontScopedUnguardedWriteCapability(); } /** * Begin a block which permits unguarded writes. You should use this very * sparingly, and only for things like logging where CSRF is not a concern. * * You must pair every call to @{method:beginUnguardedWrites} with a call to * @{method:endUnguardedWrites}: * * AphrontWriteGuard::beginUnguardedWrites(); * do_logging(); * AphrontWriteGuard::endUnguardedWrites(); * * @return void * @task disable */ public static function beginUnguardedWrites() { if (!self::$instance) { return; } self::$instance->allowDepth++; } /** * Declare that you have finished performing unguarded writes. You must * call this exactly once for each call to @{method:beginUnguardedWrites}. * * @return void * @task disable */ public static function endUnguardedWrites() { if (!self::$instance) { return; } if (self::$instance->allowDepth <= 0) { throw new Exception( "Imbalanced AphrontWriteGuard: more endUnguardedWrites() calls than ". "beginUnguardedWrites() calls."); } self::$instance->allowDepth--; } /** * Allow execution of unguarded writes. This is ONLY appropriate for use in * script contexts or other contexts where you are guaranteed to never be * vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS * if you do not understand the consequences. * * If you need to perform unguarded writes on an otherwise guarded workflow * which is vulnerable to CSRF, use @{method:beginUnguardedWrites}. * * @return void * @task disable */ public static function allowDangerousUnguardedWrites($allow) { if (self::$instance) { throw new Exception( "You can not unconditionally disable AphrontWriteGuard by calling ". "allowDangerousUnguardedWrites() while a write guard is active. Use ". "beginUnguardedWrites() to temporarily allow unguarded writes."); } self::$allowUnguardedWrites = true; } /* -( Internals )---------------------------------------------------------- */ /** * When the object is destroyed, make sure @{method:dispose} was called. * * @task internal */ public function __destruct() { if (isset(self::$instance)) { throw new Exception( "AphrontWriteGuard was not properly disposed of! Call dispose() on ". "every AphrontWriteGuard object you instantiate."); } } } diff --git a/src/channel/PhutilChannel.php b/src/channel/PhutilChannel.php index eac6ac3..3d069e2 100644 --- a/src/channel/PhutilChannel.php +++ b/src/channel/PhutilChannel.php @@ -1,334 +1,318 @@ 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("PhutilChannel->write() may only write strings!"); } $this->obuf .= $bytes; return $this; } /* -( Waiting for Activity )----------------------------------------------- */ /** * Wait (using select()) for activity on any channel. 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) Additional streams to select for read. * - 'write' (list) Additional streams to select for write. * - 'except' (list) 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 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()) { assert_instances_of($channels, 'PhutilChannel'); $read = idx($options, 'read', array()); $write = idx($options, 'write', array()); $except = idx($options, 'except', array()); $wait = idx($options, 'timeout', 1); foreach ($channels as $channel) { // If any of the 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(); $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; } foreach ($r_sockets as $socket) { $read[] = $socket; $except[] = $socket; } 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() { while (true) { $in = $this->readBytes(); if (!strlen($in)) { // Reading is blocked for now. break; } $this->ibuf .= $in; } while (strlen($this->obuf)) { $len = $this->writeBytes($this->obuf); if (!$len) { // Writing is blocked for now. break; } $this->obuf = substr($this->obuf, $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(); /** * Read from the channel's underlying I/O. * * @return string Bytes, if available. * * @task impl */ abstract protected function readBytes(); /** * 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 Read sockets. * * @task impl */ protected function getReadSockets() { return array(); } /** * Get sockets to select for writing. * * @return list Write sockets. * * @task impl */ protected function getWriteSockets() { return array(); } /** * Test state of the read buffer. * * @return bool True if the read buffer is empty. * * @task impl */ protected function isReadBufferEmpty() { return (strlen($this->ibuf) == 0); } /** * Test state of the write buffer. * * @return bool True if the write buffer is empty. * * @task impl */ protected function isWriteBufferEmpty() { return (strlen($this->obuf) == 0); } /** * 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("Channel closed while flushing output!"); } } return $this; } } diff --git a/src/channel/PhutilChannelChannel.php b/src/channel/PhutilChannelChannel.php index af4c498..34a4fc4 100644 --- a/src/channel/PhutilChannelChannel.php +++ b/src/channel/PhutilChannelChannel.php @@ -1,98 +1,82 @@ channel = $channel; } 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(); } protected function readBytes() { $this->throwOnRawByteOperations(); } protected function writeBytes($bytes) { $this->throwOnRawByteOperations(); } protected function getReadSockets() { return $this->channel->getReadSockets(); } protected function getWriteSockets() { return $this->channel->getWriteSockets(); } protected function isReadBufferEmpty() { return $this->channel->isReadBufferEmpty(); } protected function isWriteBufferEmpty() { return $this->channel->isWriteBufferEmpty(); } 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( "Do not call readBytes() or writeBytes() directly on a ". "PhutilChannelChannel. Instead, call read() or write()."); } } diff --git a/src/channel/PhutilExecChannel.php b/src/channel/PhutilExecChannel.php index cdeb384..298f4ce 100644 --- a/src/channel/PhutilExecChannel.php +++ b/src/channel/PhutilExecChannel.php @@ -1,163 +1,147 @@ 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 reasonble * 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 * * @group channel */ 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) { // 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() { 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( "Unexpected output to stderr on exec channel: {$stderr}"); } } return $stdout; } public function write($bytes) { $this->future->write($bytes, $keep_pipe = true); } protected function writeBytes($bytes) { throw new Exception("ExecFuture can not write bytes directly!"); } protected function getReadSockets() { return $this->future->getReadSockets(); } protected function getWriteSockets() { return $this->future->getWriteSockets(); } /** * 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; } } diff --git a/src/channel/PhutilJSONProtocolChannel.php b/src/channel/PhutilJSONProtocolChannel.php index f360fbe..b8d1c94 100644 --- a/src/channel/PhutilJSONProtocolChannel.php +++ b/src/channel/PhutilJSONProtocolChannel.php @@ -1,109 +1,93 @@ * * ...where 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); $obj = json_decode($data, true); if (!is_array($obj)) { throw new Exception("Failed to decode JSON object: {$data}"); } else { $objects[] = $obj; } $this->mode = self::MODE_LENGTH; $this->byteLengthOfNextChunk = self::SIZE_LENGTH; break; } } return $objects; } } diff --git a/src/channel/PhutilPHPObjectProtocolChannel.php b/src/channel/PhutilPHPObjectProtocolChannel.php index e958c9d..a94cfc8 100644 --- a/src/channel/PhutilPHPObjectProtocolChannel.php +++ b/src/channel/PhutilPHPObjectProtocolChannel.php @@ -1,106 +1,90 @@ * * ...where 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("Failed to unserialize object: {$data}"); } else { $objects[] = $obj; } $this->mode = self::MODE_LENGTH; $this->byteLengthOfNextChunk = self::SIZE_LENGTH; break; } } return $objects; } } diff --git a/src/channel/PhutilProtocolChannel.php b/src/channel/PhutilProtocolChannel.php index d2f3971..391ce40 100644 --- a/src/channel/PhutilProtocolChannel.php +++ b/src/channel/PhutilProtocolChannel.php @@ -1,151 +1,135 @@ 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 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 ($this->update()) { $message = $this->read(); if ($message !== null) { return $message; } self::waitForAny(array($this)); } throw new Exception("Channel closed while waiting for message!"); } } diff --git a/src/channel/PhutilSocketChannel.php b/src/channel/PhutilSocketChannel.php index 7e051ff..c64e6a0 100644 --- a/src/channel/PhutilSocketChannel.php +++ b/src/channel/PhutilSocketChannel.php @@ -1,169 +1,153 @@ readSocket = $read_socket; $this->writeSocket = $write_socket; } 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("stream_socket_pair() failed!"); } $x = new PhutilSocketChannel($pair[0]); $y = new PhutilSocketChannel($pair[1]); return array($x, $y); } public function isOpen() { return (bool)$this->readSocket; } protected function readBytes() { $data = @fread($this->readSocket, 4096); if ($data === false) { $this->closeSockets(); $data = ''; } // NOTE: fread() continues returning empty string after the socket is // closed, we need to check for EOF explicitly. if ($data === '') { if (feof($this->readSocket)) { $this->closeSockets(); } } return $data; } protected function writeBytes($bytes) { $socket = $this->writeSocket; if (!$socket) { $socket = $this->readSocket; } $len = @fwrite($socket, $bytes); if ($len === false) { $this->closeSockets(); return 0; } return $len; } protected function getReadSockets() { return array($this->readSocket); } protected function getWriteSockets() { if ($this->isWriteBufferEmpty()) { return array(); } else if ($this->writeSocket) { return array($this->writeSocket); } else { return array($this->readSocket); } } private function closeSockets() { foreach (array($this->readSocket, $this->writeSocket) as $socket) { if (!$socket) { continue; } // 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); } $this->readSocket = null; $this->writeSocket = null; } } diff --git a/src/channel/__tests__/PhutilJSONProtocolChannelTestCase.php b/src/channel/__tests__/PhutilJSONProtocolChannelTestCase.php index aca7f05..73a8452 100644 --- a/src/channel/__tests__/PhutilJSONProtocolChannelTestCase.php +++ b/src/channel/__tests__/PhutilJSONProtocolChannelTestCase.php @@ -1,42 +1,26 @@ mt_rand(), 'list' => array(1, 2, 3), 'null' => null, ); $xp->write($dict); $xp->flush(); $result = $yp->waitForMessage(); $this->assertEqual( $dict, $result, "Values are identical."); } } diff --git a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php index b3a98a7..4948576 100644 --- a/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php +++ b/src/channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php @@ -1,45 +1,29 @@ mt_rand(), ); $xp->write($object); $xp->flush(); $result = $yp->waitForMessage(); $this->assertEqual( true, (array)$object === (array)$result, "Values are identical."); $this->assertEqual( false, $object === $result, "Objects are not the same."); } } diff --git a/src/conduit/ConduitClient.php b/src/conduit/ConduitClient.php index 48edaec..f5c025a 100644 --- a/src/conduit/ConduitClient.php +++ b/src/conduit/ConduitClient.php @@ -1,147 +1,131 @@ connectionID; } public function __construct($uri) { $this->protocol = parse_url($uri, PHP_URL_SCHEME); $this->host = parse_url($uri, PHP_URL_HOST); $this->path = parse_url($uri, PHP_URL_PATH); $this->port = parse_url($uri, PHP_URL_PORT); 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 didReceiveResponse($method, $data) { if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall( $this->profilerCallID, array()); } 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 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 ($meta) { $params['__conduit__'] = $meta; } $port = null; if ($this->port) { $port = ':'.$this->port; } $uri = $this->protocol.'://'.$this->host.$port.'/'.$this->path.$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, ); // NOTE: If we're on Windows, the socket-based HTTPFuture won't work // properly. In theory it may be fixable, but the easier fix is just to use // the cURL-based HTTPSFuture for HTTP. We'll lose the ability to // parallelize requests but things will work correctly. $use_https_future = ($this->protocol == 'https') || phutil_is_windows(); if ($use_https_future) { $core_future = new HTTPSFuture($uri, $data); } else { $core_future = new HTTPFuture($uri, $data); } $core_future->setMethod('POST'); $core_future->setTimeout($this->timeout); $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'conduit', 'method' => $method, )); $conduit_future = new ConduitFuture($core_future); $conduit_future->setClient($this, $method); $conduit_future->isReady(); return $conduit_future; } } diff --git a/src/conduit/ConduitClientException.php b/src/conduit/ConduitClientException.php index 1805f4e..cc859e9 100644 --- a/src/conduit/ConduitClientException.php +++ b/src/conduit/ConduitClientException.php @@ -1,35 +1,19 @@ errorCode = $code; } public function getErrorCode() { return $this->errorCode; } } diff --git a/src/conduit/ConduitFuture.php b/src/conduit/ConduitFuture.php index 6d89ca1..d21316c 100644 --- a/src/conduit/ConduitFuture.php +++ b/src/conduit/ConduitFuture.php @@ -1,68 +1,52 @@ client = $client; $this->conduitMethod = $method; return $this; } protected function didReceiveResult($result) { 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 = json_decode($raw, true); if (!is_array($data)) { throw new Exception( "Host returned HTTP/200, but invalid JSON data in response to ". "a Conduit method call:\n{$raw}"); } 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; } } diff --git a/src/console/PhutilConsole.php b/src/console/PhutilConsole.php index 2dd3c98..5cfa9eb 100644 --- a/src/console/PhutilConsole.php +++ b/src/console/PhutilConsole.php @@ -1,240 +1,224 @@ } /** * 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->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("waitForMessage() called with no messages!"); } return $message; } public function getServer() { return $this->server; } } diff --git a/src/console/PhutilConsoleFormatter.php b/src/console/PhutilConsoleFormatter.php index 485f856..ca3acf2 100644 --- a/src/console/PhutilConsoleFormatter.php +++ b/src/console/PhutilConsoleFormatter.php @@ -1,108 +1,92 @@ 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) { if (phutil_is_windows()) { 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 /* ... */) { $colors = implode('|', array_keys(self::$colorCodes)); // Sequence should be preceded by start-of-string or non-backslash // escaping. $bold_re = '/(?(.*)@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.')>(.*)@sU', array('PhutilConsoleFormatter', 'replaceColorCode'), $format); } // Remove backslash escaping $format = preg_replace('/\\\\(\*\*.*\*\*|__.*__|##.*##)/sU', '\1', $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'; } } diff --git a/src/console/PhutilConsoleMessage.php b/src/console/PhutilConsoleMessage.php index 5792510..8d7e34c 100644 --- a/src/console/PhutilConsoleMessage.php +++ b/src/console/PhutilConsoleMessage.php @@ -1,49 +1,33 @@ 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; } } diff --git a/src/console/PhutilConsoleServer.php b/src/console/PhutilConsoleServer.php index 3fab36b..aa78ae9 100644 --- a/src/console/PhutilConsoleServer.php +++ b/src/console/PhutilConsoleServer.php @@ -1,132 +1,116 @@ 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; default: if ($this->handler) { return call_user_func($this->handler, $message); } else { throw new Exception( "Received unknown console message of type '{$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); } } diff --git a/src/console/PhutilConsoleServerChannel.php b/src/console/PhutilConsoleServerChannel.php index bbce0c6..dc9a2fa 100644 --- a/src/console/PhutilConsoleServerChannel.php +++ b/src/console/PhutilConsoleServerChannel.php @@ -1,31 +1,15 @@ setType(PhutilConsoleMessage::TYPE_ERR) ->setData(array('%s', $stderr)); $this->getUnderlyingChannel()->addMessage($message); } } diff --git a/src/console/PhutilConsoleStdinNotInteractiveException.php b/src/console/PhutilConsoleStdinNotInteractiveException.php index 559c03e..0aa75fe 100644 --- a/src/console/PhutilConsoleStdinNotInteractiveException.php +++ b/src/console/PhutilConsoleStdinNotInteractiveException.php @@ -1,33 +1,17 @@ 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 * @group console */ final class PhutilInteractiveEditor { 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(); $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) { 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(); } 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 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; } // Look for `editor` in PATH, some systems provide an editor which is // linked to something sensible. list($err) = exec_manual('which editor'); if (!$err) { return 'editor'; } if ($this->fallback) { return $this->fallback; } list($err) = exec_manual('which nano'); if (!$err) { return 'nano'; } throw new Exception( "Unable to launch an interactive text editor. Set the EDITOR ". "environment variable to an appropriate editor."); } } diff --git a/src/console/__tests__/PhutilConsoleWrapTestCase.php b/src/console/__tests__/PhutilConsoleWrapTestCase.php index 83a846a..a394905 100644 --- a/src/console/__tests__/PhutilConsoleWrapTestCase.php +++ b/src/console/__tests__/PhutilConsoleWrapTestCase.php @@ -1,66 +1,50 @@ assertEqual( Filesystem::readFile($dir.$file.'.expect'), phutil_console_wrap(Filesystem::readFile($dir.$file)), $file); } } } public function testConsoleWrap() { $this->assertEqual( phutil_console_format( "** ERROR ** 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!"), phutil_console_wrap( phutil_console_format( "** ERROR ** 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!")), 'ANSI escape sequences should not contribute toward wrap width.'); } public function testWrapIndent() { $turtles = <<assertEqual( $turtles, phutil_console_wrap( rtrim(str_repeat('turtle ', 20)), $indent = 20)); } } diff --git a/src/console/format.php b/src/console/format.php index 023c39e..e40cdbd 100644 --- a/src/console/format.php +++ b/src/console/format.php @@ -1,243 +1,227 @@ /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. * @return string Wrapped text. * * @group console */ function phutil_console_wrap($text, $indent = 0) { $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) { $lines[$idx] = $pre.implode('', $line); } return implode('', $lines); } /** * @group console */ 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. * @group console */ function phutil_console_get_terminal_width() { if (phutil_is_windows()) { // TODO: Figure out how to get this working in 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 = phutil_passthru('tput cols > %s', $tmp); if ($err) { return null; } try { $cols = Filesystem::readFile($tmp); } catch (FilesystemException $ex) { return null; } $cols = (int)$cols; if (!$cols) { return null; } return $cols; } diff --git a/src/daemon/PhutilDaemon.php b/src/daemon/PhutilDaemon.php index be15d74..1b04df9 100644 --- a/src/daemon/PhutilDaemon.php +++ b/src/daemon/PhutilDaemon.php @@ -1,132 +1,116 @@ verbose = $verbose; return $this; } final public function getVerbose() { return $this->verbose; } private static $sighandlerInstalled; final public function __construct(array $argv) { declare(ticks = 1); $this->argv = $argv; if (!self::$sighandlerInstalled) { self::$sighandlerInstalled = true; pcntl_signal(SIGINT, __CLASS__.'::exitOnSignal'); pcntl_signal(SIGTERM, __CLASS__.'::exitOnSignal'); } // Without discard mode, this consumes unbounded amounts of memory. Keep // memory bounded. PhutilServiceProfiler::getInstance()->enableDiscardMode(); } final public function stillWorking() { if (!posix_isatty(STDOUT)) { posix_kill(posix_getppid(), SIGUSR1); } if ($this->traceMemory) { $memuse = number_format(memory_get_usage() / 1024, 1); $daemon = get_class($this); fprintf(STDERR, '%s', " {$daemon} Memory Usage: {$memuse} KB\n"); } } final protected function sleep($duration) { $this->stillWorking(); while ($duration > 0) { sleep(min($duration, 60)); $duration -= 60; $this->stillWorking(); } } public static function exitOnSignal($signo) { // Normally, PHP doesn't invoke destructors when existing in response to // a signal. This forces it to do so, so we have a fighting chance of // releasing any locks, leases or resources on our way out. exit(128 + $signo); } final protected function getArgv() { return $this->argv; } final public function execute() { $this->willRun(); $this->run(); } abstract protected function run(); final public function setTraceMemory() { $this->traceMemory = true; return $this; } final public function getTraceMemory() { return $this->traceMemory; } final public function setTraceMode() { $this->traceMode = true; PhutilServiceProfiler::installEchoListener(); $this->didSetTraceMode(); return $this; } final public function getTraceMode() { return $this->traceMode; } protected function willRun() { // This is a hook for subclasses. } protected function didSetTraceMode() { // This is a hook for subclasses. } final protected function log($message) { if ($this->verbose) { $daemon = get_class($this); fprintf(STDERR, '%s', " {$daemon} {$message}\n"); } } } diff --git a/src/daemon/PhutilDaemonOverseer.php b/src/daemon/PhutilDaemonOverseer.php index a437699..c402032 100644 --- a/src/daemon/PhutilDaemonOverseer.php +++ b/src/daemon/PhutilDaemonOverseer.php @@ -1,377 +1,361 @@ enableDiscardMode(); $original_argv = $argv; $args = new PhutilArgumentParser($argv); $args->setTagline('daemon overseer'); $args->setSynopsis(<<parsePartial( array( array( 'name' => 'trace', 'help' => 'Enable debug tracing.', ), array( 'name' => 'trace-memory', 'help' => 'Enable debug memory tracing.', ), array( 'name' => 'log', 'param' => 'file', 'help' => 'Send output to __file__.', ), array( 'name' => 'daemonize', 'help' => 'Run in the background.', ), array( 'name' => 'phd', 'param' => 'dir', 'help' => 'Write PID information to __dir__.', ), array( 'name' => 'conduit-uri', 'param' => 'uri', 'help' => 'Send logs to Conduit on __uri__.' ), array( 'name' => 'verbose', 'help' => 'Enable verbose activity logging.', ), )); $argv = $args->getUnconsumedArgumentVector(); $this->daemon = array_shift($argv); if (!$this->daemon) { $args->printHelpAndExit(); } if ($args->getArg('trace')) { $this->traceMode = true; array_unshift($argv, '--trace'); } if ($args->getArg('trace-memory')) { $this->traceMode = true; $this->traceMemory = true; array_unshift($argv, '--trace-memory'); } $log = $args->getArg('log'); if ($log) { ini_set('error_log', $log); array_unshift($argv, '--log='.$log); } $verbose = $args->getArg('verbose'); if ($verbose) { $this->verbose = true; array_unshift($argv, '--verbose'); } $this->daemonize = $args->getArg('daemonize'); $this->phddir = $args->getArg('phd'); $this->conduitURI = $args->getArg('conduit-uri'); $this->argv = $argv; error_log("Bringing daemon '{$this->daemon}' online..."); if (self::$instance) { throw new Exception( "You may not instantiate more than one Overseer per process."); } self::$instance = $this; if ($this->daemonize) { // We need to get rid of these or the daemon will hang when we TERM it // waiting for something to read the buffers. TODO: Learn how unix works. fclose(STDOUT); fclose(STDERR); ob_start(); $pid = pcntl_fork(); if ($pid === -1) { throw new Exception("Unable to fork!"); } else if ($pid) { exit(0); } } if ($this->phddir) { $desc = array( 'name' => $this->daemon, 'pid' => getmypid(), 'start' => time(), ); Filesystem::writeFile( $this->phddir.'/daemon.'.getmypid(), json_encode($desc)); } if ($this->conduitURI) { $this->conduit = new ConduitClient($this->conduitURI); $this->daemonLogID = $this->conduit->callMethodSynchronous( 'daemon.launched', array( 'daemon' => $this->daemon, 'host' => php_uname('n'), 'pid' => getmypid(), 'argv' => json_encode(array_slice($original_argv, 1)), )); } declare(ticks = 1); pcntl_signal(SIGUSR1, array($this, 'didReceiveKeepaliveSignal')); pcntl_signal(SIGINT, array($this, 'didReceiveTerminalSignal')); pcntl_signal(SIGTERM, array($this, 'didReceiveTerminalSignal')); } public function run() { if ($this->shouldRunSilently()) { echo "Running daemon '{$this->daemon}' silently. Use '--trace' or ". "'--verbose' to produce debugging output.\n"; } $root = phutil_get_library_root('phutil'); $root = dirname($root); $exec_dir = $root.'/scripts/daemon/exec/'; // NOTE: PHP implements proc_open() by running 'sh -c'. On most systems this // is bash, but on Ubuntu it's dash. When you proc_open() using bash, you // get one new process (the command you ran). When you proc_open() using // dash, you get two new processes: the command you ran and a parent // "dash -c" (or "sh -c") process. This means that the child process's PID // is actually the 'dash' PID, not the command's PID. To avoid this, use // 'exec' to replace the shell process with the real process; without this, // the child will call posix_getppid(), be given the pid of the 'sh -c' // process, and send it SIGUSR1 to keepalive which will terminate it // immediately. We also won't be able to do process group management because // the shell process won't properly posix_setsid() so the pgid of the child // won't be meaningful. $exec_daemon = './exec_daemon.php'; $argv = $this->argv; array_unshift($argv, 'exec', $exec_daemon, $this->daemon); foreach ($argv as $k => $arg) { $argv[$k] = escapeshellarg($arg); } $command = implode(' ', $argv); while (true) { $this->logMessage('INIT', 'Starting process.'); $future = new ExecFuture($command); $future->setCWD($exec_dir); $future->setStdoutSizeLimit($this->captureBufferSize); $future->setStderrSizeLimit($this->captureBufferSize); $this->deadline = time() + $this->deadlineTimeout; $this->heartbeat = time() + self::HEARTBEAT_WAIT; $future->isReady(); $this->childPID = $future->getPID(); do { do { if ($this->traceMemory) { $memuse = number_format(memory_get_usage() / 1024, 1); $this->logMessage('RAMS', 'Overseer Memory Usage: '.$memuse.' KB'); } // We need a shortish timeout here so we can run the tick handler // frequently in order to process signals. $result = $future->resolve(1); list($stdout, $stderr) = $future->read(); $stdout = trim($stdout); $stderr = trim($stderr); if (strlen($stdout)) { $this->logMessage('STDO', $stdout, $stdout); } if (strlen($stderr)) { $this->logMessage('STDE', $stderr, $stderr); } $future->discardBuffers(); if ($result !== null) { list($err) = $result; if ($err) { $this->logMessage( 'FAIL', 'Process exited with error '.$err.'.', $err); } else { $this->logMessage('DONE', 'Process exited successfully.'); } break 2; } if ($this->heartbeat < time()) { $this->heartbeat = time() + self::HEARTBEAT_WAIT; if ($this->conduitURI) { try { $this->conduit = new ConduitClient($this->conduitURI); $this->conduit->callMethodSynchronous( 'daemon.setstatus', array( 'daemonLogID' => $this->daemonLogID, 'status' => 'run', )); } catch (Exception $ex) { } } } } while (time() < $this->deadline); $this->logMessage('HANG', 'Hang detected. Restarting process.'); $this->annihilateProcessGroup(); } while (false); $this->logMessage('WAIT', 'Waiting to restart process.'); sleep($this->restartDelay); } } public function didReceiveKeepaliveSignal($signo) { $this->deadline = time() + $this->deadlineTimeout; } public function didReceiveTerminalSignal($signo) { if ($this->signaled) { exit(128 + $signo); } echo "\n>>> Shutting down...\n"; fflush(STDOUT); fflush(STDERR); fclose(STDOUT); fclose(STDERR); $this->signaled = true; $this->annihilateProcessGroup(); if ($this->conduitURI) { try { $this->conduit = new ConduitClient($this->conduitURI); $this->conduit->callMethodSynchronous( 'daemon.setstatus', array( 'daemonLogID' => $this->daemonLogID, 'status' => 'exit', )); } catch (Exception $ex) { } } exit(128 + $signo); } private function logMessage($type, $message, $remote = null) { if (!$this->shouldRunSilently()) { echo date('Y-m-d g:i:s A').' ['.$type.'] '.$message."\n"; } if ($this->conduit) { // TODO: This is kind of sketchy to do without any timeouts since a // conduit server hang could throw a wrench into things. try { $this->conduit->callMethodSynchronous( 'daemon.log', array( 'daemonLogID' => $this->daemonLogID, 'type' => $type, 'message' => $remote, )); } catch (Exception $ex) { // TOOD: Send this somewhere useful instead of eating it. } } } private function shouldRunSilently() { if ($this->traceMode || $this->verbose) { return false; } else { return true; } } private function annihilateProcessGroup() { $pid = $this->childPID; $pgid = posix_getpgid($pid); if ($pid && $pgid) { // NOTE: On Ubuntu, 'kill' does not recognize the use of "--" to // explicitly delineate PID/PGIDs from signals. We don't actually need it, // so use the implicit "kill -TERM -pgid" form instead of the explicit // "kill -TERM -- -pgid" form. exec("kill -TERM -{$pgid}"); sleep($this->killDelay); // On OSX, we'll get a permission error on stderr if the SIGTERM was // successful in ending the life of the process group, presumably because // all that's left is the daemon itself as a zombie waiting for us to // reap it. However, we still need to issue this command for process // groups that resist SIGTERM. Rather than trying to figure out if the // process group is still around or not, just SIGKILL unconditionally and // ignore any error which may be raised. exec("kill -KILL -{$pgid} 2>/dev/null"); $this->childPID = null; } } } diff --git a/src/daemon/torture/PhutilExcessiveServiceCallsDaemon.php b/src/daemon/torture/PhutilExcessiveServiceCallsDaemon.php index d5d878b..54344b0 100644 --- a/src/daemon/torture/PhutilExcessiveServiceCallsDaemon.php +++ b/src/daemon/torture/PhutilExcessiveServiceCallsDaemon.php @@ -1,33 +1,17 @@ stillWorking(); } } } diff --git a/src/daemon/torture/PhutilFatalDaemon.php b/src/daemon/torture/PhutilFatalDaemon.php index e987d38..ce9b20d 100644 --- a/src/daemon/torture/PhutilFatalDaemon.php +++ b/src/daemon/torture/PhutilFatalDaemon.php @@ -1,30 +1,14 @@ log(date('r')); $this->stillWorking(); sleep(1); } } } diff --git a/src/daemon/torture/PhutilProcessGroupDaemon.php b/src/daemon/torture/PhutilProcessGroupDaemon.php index 03b088d..74b377c 100644 --- a/src/daemon/torture/PhutilProcessGroupDaemon.php +++ b/src/daemon/torture/PhutilProcessGroupDaemon.php @@ -1,33 +1,17 @@ doSomething(); * $success = true; * break; * } catch (Exception $ex) { * $exceptions[get_class($engine)] = $ex; * } * } * * if (!$success) { * throw new PhutilAggregateException("All engines failed:", $exceptions); * } * * @concrete-extensible * @group error */ 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; } } diff --git a/src/error/PhutilErrorHandler.php b/src/error/PhutilErrorHandler.php index a4afad5..c20b93f 100644 --- a/src/error/PhutilErrorHandler.php +++ b/src/error/PhutilErrorHandler.php @@ -1,310 +1,294 @@ $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 Uncaught exception object. * @return void * @task internal */ public static function handleException(Exception $ex) { self::dispatchErrorMessage( self::EXCEPTION, $ex, array( 'file' => $ex->getFile(), 'line' => $ex->getLine(), 'trace' => $ex->getTrace(), '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) { foreach ($trace as $key => $entry) { $line = ' #'.$key.' '; if (isset($entry['class'])) { $line .= $entry['class'].'::'; } $line .= idx($entry, 'function', ''); if (isset($entry['args'])) { $args = array(); foreach ($entry['args'] as $arg) { $args[] = PhutilReadableSerializer::printShort($arg); } $line .= '('.implode(', ', $args).')'; } if (isset($entry['file'])) { $line .= ' called at ['.$entry['file'].':'.$entry['line'].']'; } error_log($line); } } /** * 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("%F %T"); switch ($event) { case PhutilErrorHandler::ERROR: if (error_reporting() === 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. break; } $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 PhutilErrorHandler::EXCEPTION: $default_message = sprintf( '[%s] EXCEPTION: %s at [%s:%d]', $timestamp, '('.get_class($value).') '.$value->getMessage(), $value->getFile(), $value->getLine()); $metadata['default_message'] = $default_message; error_log($default_message); self::outputStacktrace($value->getTrace()); break; case PhutilErrorHandler::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 PhutilErrorHandler::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('Unknown event '.$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; } } } diff --git a/src/error/PhutilOpaqueEnvelope.php b/src/error/PhutilOpaqueEnvelope.php index e219590..af10083 100644 --- a/src/error/PhutilOpaqueEnvelope.php +++ b/src/error/PhutilOpaqueEnvelope.php @@ -1,88 +1,72 @@ 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 { 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 ''; } /* -( 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; } } diff --git a/src/error/PhutilOpaqueEnvelopeKey.php b/src/error/PhutilOpaqueEnvelopeKey.php index c4de0ac..85705b7 100644 --- a/src/error/PhutilOpaqueEnvelopeKey.php +++ b/src/error/PhutilOpaqueEnvelopeKey.php @@ -1,65 +1,49 @@ } /** * @task internal */ public static function getKey() { if (self::$key === null) { try { self::$key = Filesystem::readRandomBytes(128); } catch (Exception $ex) { // NOTE: We can't throw here! Otherwise we might get a stack trace // including the string that was passed to PhutilOpaqueEnvelope's // constructor. Just die() instead. die( "Unable to read random bytes in PhutilOpaqueEnvelope. (This ". "causes an immediate process exit to avoid leaking the envelope ". "contents in a stack trace.)"); } } return self::$key; } } diff --git a/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php b/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php index 87ef8a1..b29e88d 100644 --- a/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php +++ b/src/error/__tests__/PhutilOpaqueEnvelopeTestCase.php @@ -1,73 +1,57 @@ assertEqual( false, strpos(var_export($envelope, true), $secret)); $this->assertEqual( false, strpos(print_r($envelope, true), $secret)); ob_start(); var_dump($envelope); $dump = ob_get_clean(); $this->assertEqual( false, strpos($dump, $secret)); try { $this->throwTrace($envelope); } catch (Exception $ex) { $trace = $ex->getTrace(); $this->assertEqual( false, strpos(print_r($trace, true), $secret)); } $backtrace = $this->getBacktrace($envelope); $this->assertEqual( false, 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(); } } diff --git a/src/error/phlog.php b/src/error/phlog.php index 3f1bfb5..d744db5 100644 --- a/src/error/phlog.php +++ b/src/error/phlog.php @@ -1,84 +1,68 @@ $file, 'line' => $line, 'trace' => $trace)); 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 * @group error */ function phutil_error_listener_example($event, $value, array $metadata) { throw new Exception("This is just an example function!"); } diff --git a/src/events/PhutilEvent.php b/src/events/PhutilEvent.php index 5e95bc8..f1ebec7 100644 --- a/src/events/PhutilEvent.php +++ b/src/events/PhutilEvent.php @@ -1,61 +1,45 @@ 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; } } diff --git a/src/events/PhutilEventEngine.php b/src/events/PhutilEventEngine.php index ea38865..676111b 100644 --- a/src/events/PhutilEventEngine.php +++ b/src/events/PhutilEventEngine.php @@ -1,94 +1,78 @@ } 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()); } } diff --git a/src/events/PhutilEventListener.php b/src/events/PhutilEventListener.php index 27df682..15bbe3f 100644 --- a/src/events/PhutilEventListener.php +++ b/src/events/PhutilEventListener.php @@ -1,57 +1,41 @@ } 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; } } diff --git a/src/events/constant/PhutilEventConstants.php b/src/events/constant/PhutilEventConstants.php index 5e8921f..64b9726 100644 --- a/src/events/constant/PhutilEventConstants.php +++ b/src/events/constant/PhutilEventConstants.php @@ -1,24 +1,8 @@ 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 * @group filesystem */ final class FileFinder { private $root; private $exclude = array(); private $paths = array(); private $suffix = array(); private $type; private $generateChecksums = false; private $followSymlinks; /** * Create a new FileFinder. * * @param string Root directory to find files beneath. * @return this * @task create */ public function __construct($root) { $this->root = $root; } /** * @task config */ public function excludePath($path) { $this->exclude[] = $path; 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; } /** * @task exec */ public function find() { $args = array(); $command = array(); $command[] = '(cd %s; '; $args[] = $this->root; $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->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( "Unable to locate the md5/md5sum binary for this system."); } } $command[] = ' | xargs -0 -n512 '.$md5sum_binary; } $command[] = ')'; list($stdout) = call_user_func_array( 'execx', array_merge( array(implode(' ', $command)), $args)); $stdout = trim($stdout); if (!strlen($stdout)) { return array(); } if (!$this->generateChecksums) { return explode("\0", $stdout); } else { $map = array(); foreach (explode("\n", $stdout) as $line) { $file = substr($line, 34); if ($file == '-') { continue; } // This mess is to make this class work on both mainline Linux systems // and OSX, which has subtly different 'find' semantics. $file = $this->root.ltrim($file, '.'); $map[$file] = substr($line, 0, 32); } return $map; } } /** * @task internal */ private function generateList($flag, array $items) { $items = array_map('escapeshellarg', $items); foreach ($items as $key => $item) { $items[$key] = '-'.$flag.' '.$item; } $items = implode(' -o ', $items); return '\\( '.$items.' \\)'; } } diff --git a/src/filesystem/FileList.php b/src/filesystem/FileList.php index ebf4353..192b980 100644 --- a/src/filesystem/FileList.php +++ b/src/filesystem/FileList.php @@ -1,109 +1,93 @@ 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 * @group filesystem */ final class FileList { 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; } } diff --git a/src/filesystem/Filesystem.php b/src/filesystem/Filesystem.php index e442333..c4d0c1f 100644 --- a/src/filesystem/Filesystem.php +++ b/src/filesystem/Filesystem.php @@ -1,911 +1,895 @@ GetRandom($number_of_bytes); } catch (Exception $ex) { throw new FilesystemException( 'CAPICOM.Utilities.1', 'Unable to read random bytes through CAPICOM!'); } } $urandom = @fopen('/dev/urandom', 'rb'); if (!$urandom) { throw new FilesystemException( '/dev/urandom', 'Failed to open /dev/urandom for reading!'); } $data = @fread($urandom, $number_of_bytes); if (strlen($data) != $number_of_bytes) { throw new FilesystemException( '/dev/urandom', 'Failed to read random bytes!'); } @fclose($urandom); return $data; } /** * Read random alphanumeric characters from /dev/urandom or equivalent. This * method operates like @{method:readRandomBytes} but produces alphanumeric * output (a-z, 0-9) so it's appropriate for use in URIs and other contexts * where it needs to be human readable. * * @param int Number of characters to read. * @return string Random character string of the provided length. * * @task file */ public static function readRandomCharacters($number_of_characters) { // NOTE: To produce the character string, we generate a random byte string // of the same length, select the high 5 bits from each byte, and // map that to 32 alphanumeric characters. This could be improved (we // could improve entropy per character with base-62, and some entropy // sources might be less entropic if we discard the low bits) but for // reasonable cases where we have a good entropy source and are just // generating some kind of human-readable secret this should be more than // sufficient and is vastly simpler than trying to do bit fiddling. $map = array_merge(range('a', 'z'), range('2', '7')); $result = ''; $bytes = self::readRandomBytes($number_of_characters); for ($ii = 0; $ii < $number_of_characters; $ii++) { $result .= $map[ord($bytes[$ii]) >> 3]; } return $result; } /** * Identify the MIME type of a file. This returns only the MIME type (like * text/plain), not the encoding (like charset=utf-8). * * @param string Path to the file to examine. * @param string Optional default mime type to return if the file's mime * type can not be identified. * @return string File mime type. * * @task file * * @phutil-external-symbol function mime_content_type * @phutil-external-symbol function finfo_open * @phutil-external-symbol function finfo_file */ public static function getMimeType( $path, $default = 'application/octet-stream') { $path = self::resolvePath($path); self::assertExists($path); self::assertIsFile($path); self::assertReadable($path); $mime_type = null; // Fileinfo is the best approach since it doesn't rely on `file`, but // it isn't builtin for older versions of PHP. if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME); if ($finfo) { $result = finfo_file($finfo, $path); if ($result !== false) { $mime_type = $result; } } } // If we failed Fileinfo, try `file`. This works well but not all systems // have the binary. if ($mime_type === null) { list($err, $stdout) = exec_manual( 'file --brief --mime %s', $path); if (!$err) { $mime_type = trim($stdout); } } // If we didn't get anywhere, try the deprecated mime_content_type() // function. if ($mime_type === null) { if (function_exists('mime_content_type')) { $result = mime_content_type($path); if ($result !== false) { $mime_type = $result; } } } // If we come back with an encoding, strip it off. if (strpos($mime_type, ';') !== false) { list($type, $encoding) = explode(';', $mime_type, 2); $mime_type = $type; } if ($mime_type === null) { $mime_type = $default; } return $mime_type; } /* -( Directories )-------------------------------------------------------- */ /** * Create a directory in a manner similar to mkdir(), but throw detailed * exceptions on failure. * * @param string Path 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 !== false && $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 FilesystemException( $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 (is_link($path)) { $path = realpath($path); } $walk = array(); $parts = explode(DIRECTORY_SEPARATOR, $path); foreach ($parts as $k => $part) { if (!strlen($part)) { unset($parts[$k]); } } do { if (phutil_is_windows()) { $walk[] = implode(DIRECTORY_SEPARATOR, $parts); } else { $walk[] = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $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 (phutil_is_windows()) { $is_absolute = preg_match('/^[A-Z]+:/', $path); } else { $is_absolute = !strncmp($path, DIRECTORY_SEPARATOR, 1); } if (!$is_absolute) { if (!$relative_to) { $relative_to = getcwd(); } $path = $relative_to.DIRECTORY_SEPARATOR.$path; } if (is_link($path)) { $parent_realpath = realpath(dirname($path)); if ($parent_realpath !== false) { return $parent_realpath.DIRECTORY_SEPARATOR.basename($path); } } $realpath = realpath($path); if ($realpath !== false) { return $realpath; } // This won't work if the file doesn't exist or is on an unreadable mount // or something crazy like that. Try to resolve a parent so we at least // cover the nonexistent file case. $parts = explode(DIRECTORY_SEPARATOR, trim($path, DIRECTORY_SEPARATOR)); while (end($parts) !== false) { array_pop($parts); if (phutil_is_windows()) { $attempt = implode(DIRECTORY_SEPARATOR, $parts); } else { $attempt = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $parts); } $realpath = realpath($attempt); if ($realpath !== false) { $path = $realpath.substr($path, strlen($attempt)); break; } } return $path; } /** * Test whether a path is descendant from some root path after resolving all * symlinks and removing artifacts. Both paths must exists for the relation * to obtain. A path is always a descendant of itself as long as it exists. * * @param string 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 = getcwd(); } foreach (array($pwd, self::resolvePath($pwd)) as $parent) { $parent = rtrim($parent, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; $len = strlen($parent); if (!strncmp($parent, $path, $len)) { $path = substr($path, $len); return $path; } } return $path; } /** * Determine whether or not a path exists in the filesystem. This differs from * file_exists() in that it returns true for symlinks. This method does not * attempt to resolve paths before testing them. * * @param string 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) 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."); } } } diff --git a/src/filesystem/FilesystemException.php b/src/filesystem/FilesystemException.php index 883d769..5f417ca 100644 --- a/src/filesystem/FilesystemException.php +++ b/src/filesystem/FilesystemException.php @@ -1,52 +1,36 @@ 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; } } diff --git a/src/filesystem/PhutilDeferredLog.php b/src/filesystem/PhutilDeferredLog.php index 5bfc4ca..6552359 100644 --- a/src/filesystem/PhutilDeferredLog.php +++ b/src/filesystem/PhutilDeferredLog.php @@ -1,193 +1,177 @@ 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 internal Internals * @group filesystem */ final class PhutilDeferredLog { private $file; private $format; private $data; private $didWrite; /* -( 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 The file the entry should be written to. * @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; } public function getData($key, $default = null) { return idx($this->data, $key, $default); } /** * When the log object is destroyed, it writes if it hasn't written yet. * @task log */ 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 log */ public function write() { if ($this->didWrite) { return $this; } $line = $this->format(); $ok = @file_put_contents( $this->file, $line, FILE_APPEND | LOCK_EX); if ($ok === false) { throw new Exception( "Unable to write to logfile '{$this->file}'!"); } $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"; } } diff --git a/src/filesystem/PhutilDirectoryFixture.php b/src/filesystem/PhutilDirectoryFixture.php index ad45cb1..0767af0 100644 --- a/src/filesystem/PhutilDirectoryFixture.php +++ b/src/filesystem/PhutilDirectoryFixture.php @@ -1,69 +1,53 @@ getPath(), Filesystem::resolvePath($archive)); return $obj; } public static function newEmptyFixture() { $obj = new PhutilDirectoryFixture(); $obj->path = Filesystem::createTemporaryDirectory(); return $obj; } private function __construct() { // } 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, 'Failed to overwrite file.'); } return $this; } } diff --git a/src/filesystem/PhutilFileLock.php b/src/filesystem/PhutilFileLock.php index e819865..61fd231 100644 --- a/src/filesystem/PhutilFileLock.php +++ b/src/filesystem/PhutilFileLock.php @@ -1,136 +1,120 @@ lock(); * * do_contentious_things(); * * $lock->unlock(); * * For more information on locks, see @{class:PhutilLock}. * * @task construct Constructing Locks * @task impl Implementation * * @group filesystem */ 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, "Unable to open lock '{$path}' for writing!"); } $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("Unable to unlock file!"); } $ok = fclose($this->handle); if (!$ok) { throw new Exception("Unable to close file!"); } $this->handle = null; } } diff --git a/src/filesystem/PhutilFileTree.php b/src/filesystem/PhutilFileTree.php index 86ede73..ba75e67 100644 --- a/src/filesystem/PhutilFileTree.php +++ b/src/filesystem/PhutilFileTree.php @@ -1,128 +1,112 @@ 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( "Duplicate insertion for path '{$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 $name => $node) { if ($found) { return $node; } if ($this->name == $name) { $found = true; } } return null; } } diff --git a/src/filesystem/PhutilLock.php b/src/filesystem/PhutilLock.php index a3908e7..cf6e6db 100644 --- a/src/filesystem/PhutilLock.php +++ b/src/filesystem/PhutilLock.php @@ -1,252 +1,236 @@ 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 * * @group filesystem */ abstract class PhutilLock { 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('PhutilLock', 'unlockAll')); self::$registeredShutdownFunction = true; } $name = $lock->getName(); if (self::getLock($name)) { throw new Exception("Lock '{$name}' is already registered!"); } 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( "Lock '{$name}' has already been locked by this process."); } $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( "Lock '{$name} is not locked by this process!"); } $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(); } } } } diff --git a/src/filesystem/PhutilLockException.php b/src/filesystem/PhutilLockException.php index 398f07e..bbc5cc2 100644 --- a/src/filesystem/PhutilLockException.php +++ b/src/filesystem/PhutilLockException.php @@ -1,19 +1,3 @@ dir = Filesystem::createTemporaryDirectory(); if ($filename === null) { $this->file = tempnam($this->dir, getmypid().'-'); } else { $this->file = $this->dir.'/'.$filename; } 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->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); } } diff --git a/src/filesystem/__tests__/PhutilDeferredLogTestCase.php b/src/filesystem/__tests__/PhutilDeferredLogTestCase.php index d3ffe75..67e9cf5 100644 --- a/src/filesystem/__tests__/PhutilDeferredLogTestCase.php +++ b/src/filesystem/__tests__/PhutilDeferredLogTestCase.php @@ -1,149 +1,133 @@ checkLog( "derp\n", "derp", array()); $this->checkLog( "[20 Aug 1984] alincoln\n", "[%T] %u", array( 'T' => '20 Aug 1984', 'u' => 'alincoln', )); $this->checkLog( "%%%%%\n", "%%%%%%%%%%", array( '%' => '%', )); $this->checkLog( "\\000\\001\\002\n", "%a%b%c", array( 'a' => chr(0), 'b' => chr(1), 'c' => chr(2), )); $this->checkLog( "Download: 100%\n", "Download: %C", array( 'C' => '100%', )); $this->checkLog( "- bee -\n", "%a %b %c", array( 'b' => 'bee', )); $this->checkLog( "\\\\\n", "%b", array( 'b' => '\\', )); $this->checkLog( "a\t\\t\n", "%a\t%b", array( 'a' => 'a', 'b' => "\t", )); $this->checkLog( "\1ab\n", "\1a%a", array( 'a' => 'b', )); $this->checkLog( "a % xb\n", "%a %% x%b", array( 'a' => 'a', 'b' => 'b', )); } public function testLogWriteFailure() { $caught = null; try { if (phutil_is_hiphop_runtime()) { // In HipHop exceptions thrown in destructors are not normally // catchable, so call __destruct() explicitly. $log = new PhutilDeferredLog('/derp/derp/derp/derp/derp', 'derp'); $log->__destruct(); } else { new PhutilDeferredLog('/derp/derp/derp/derp/derp', 'derp'); } } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testManyWriters() { $root = phutil_get_library_root('phutil').'/../'; $bin = $root.'scripts/test/deferred_log.php'; $n_writers = 3; $n_lines = 8; $tmp = new TempFile(); $futures = array(); for ($ii = 0; $ii < $n_writers; $ii++) { $futures[] = new ExecFuture("%s %d %s", $bin, $n_lines, (string)$tmp); } Futures($futures)->resolveAll(); $this->assertEqual( str_repeat("abcdefghijklmnopqrstuvwxyz\n", $n_writers * $n_lines), Filesystem::readFile($tmp)); } private function checkLog($expect, $format, $data) { $tmp = new TempFile(); $log = new PhutilDeferredLog($tmp, $format); $log->setData($data); unset($log); $this->assertEqual($expect, Filesystem::readFile($tmp), $format); } } diff --git a/src/filesystem/__tests__/PhutilFileLockTestCase.php b/src/filesystem/__tests__/PhutilFileLockTestCase.php index be02bc7..372c6df 100644 --- a/src/filesystem/__tests__/PhutilFileLockTestCase.php +++ b/src/filesystem/__tests__/PhutilFileLockTestCase.php @@ -1,228 +1,212 @@ assertEqual( true, $this->lockTest($file)); $this->assertEqual( true, $this->lockTest($file)); } public function testLockHolding() { // When a process is holding a lock, other processes should be unable // to acquire it. $file = new TempFile(); $hold = $this->holdLock($file); $this->assertEqual( false, $this->lockTest($file)); $hold->resolveKill(); $this->assertEqual( true, $this->lockTest($file)); } public function testInProcessLocking() { // Other processes should be unable to lock a file if we hold the lock. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $this->assertEqual( false, $this->lockTest($file)); $lock->unlock(); $this->assertEqual( true, $this->lockTest($file)); } public function testInProcessHolding() { // We should be unable to lock a file if another process is holding the // lock. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $hold = $this->holdLock($file); $caught = null; try { $lock->lock(); } catch (PhutilLockException $ex) { $caught = $ex; } $this->assertEqual( true, ($caught instanceof PhutilLockException)); $hold->resolveKill(); $this->assertEqual( true, $this->lockTest($file)); $lock->lock(); $lock->unlock(); } public function testRelock() { // Trying to lock a file twice should throw an exception. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $caught = null; try { $lock->lock(); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, ($caught instanceof Exception)); } public function testExcessiveUnlock() { // Trying to unlock a file twice should throw an exception. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $lock->unlock(); $caught = null; try { $lock->unlock(); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, ($caught instanceof Exception)); } public function testUnlockAll() { // unlockAll() should release all locks. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $lock->lock(); $this->assertEqual(false, $this->lockTest($file)); PhutilFileLock::unlockAll(); $this->assertEqual(true, $this->lockTest($file)); // Calling this again shouldn't do anything bad. PhutilFileLock::unlockAll(); $this->assertEqual(true, $this->lockTest($file)); $lock->lock(); $lock->unlock(); } public function testIsLocked() { // isLocked() should report lock status accurately. $file = new TempFile(); $lock = PhutilFileLock::newForPath($file); $this->assertEqual(false, $lock->isLocked()); $lock->lock(); $this->assertEqual(true, $lock->isLocked()); $lock->unlock(); $this->assertEqual(false, $lock->isLocked()); } private function lockTest($file) { list($err) = $this->buildLockFuture('--test', $file)->resolve(); return ($err == 0); } private function holdLock($file) { $future = $this->buildLockFuture('--hold', $file); // We can't return until we're sure the subprocess has had time to acquire // the lock. Since actually testing for the lock would be kind of silly // and guarantee that we loop forever if the locking primitive broke, // watch stdout for a *claim* that it has acquired the lock instead. // Make sure we don't loop forever, no matter how bad things get. $future->setTimeout(30); $buf = ''; while (!$future->isReady()) { list($stdout) = $future->read(); $buf .= $stdout; if (strpos($buf, 'LOCK ACQUIRED') !== false) { return $future; } } throw new Exception("Unable to hold lock in external process!"); } private function buildLockFuture($flags, $file) { $root = dirname(phutil_get_library_root('phutil')); $bin = $root.'/scripts/utils/lock.php'; $future = new ExecFuture('%s %C %s', $bin, $flags, $file); $future->start(); return $future; } } diff --git a/src/filesystem/linesofalarge/LinesOfALarge.php b/src/filesystem/linesofalarge/LinesOfALarge.php index a1f29bc..4afdc16 100644 --- a/src/filesystem/linesofalarge/LinesOfALarge.php +++ b/src/filesystem/linesofalarge/LinesOfALarge.php @@ -1,227 +1,211 @@ delimiter = $character; return $this; } /* -( Internals )---------------------------------------------------------- */ /** * Hook, called before @{method:rewind()}. Allows a concrete implementation * to open resources or reset state. * * @return void * @task internals */ abstract protected function willRewind(); /** * Called when the iterator needs more data. The subclass should return more * data, or empty string to indicate end-of-stream. * * @return string Data, or empty string for end-of-stream. * @task internals */ abstract protected function readMore(); /* -( Iterator Interface )------------------------------------------------- */ /** * @task iterator */ final public function rewind() { $this->willRewind(); $this->buf = ''; $this->pos = 0; $this->num = 0; $this->eof = false; $this->valid = true; $this->next(); } /** * @task iterator */ final public function key() { return $this->num; } /** * @task iterator */ final public function current() { return $this->line; } /** * @task iterator */ final public function valid() { return $this->valid; } /** * @task iterator */ final public function next() { // Consume the stream a chunk at a time into an internal buffer, then // read lines out of that buffer. This gives us flexibility (stream sources // only need to be able to read blocks of bytes) and performance (we can // read in reasonably-sized chunks of many lines), at the cost of some // complexity in buffer management. // We do this in a loop to avoid recursion when consuming more bytes, in // case the size of a line is very large compared to the chunk size we // read. while (true) { if (strlen($this->buf)) { // If we already have some data buffered, try to get the next line from // the buffer. Search through the buffer for a delimiter. This should be // the common case. $endl = strpos($this->buf, $this->delimiter, $this->pos); if ($endl !== false) { // We found a delimiter, so return the line it delimits. We leave // the buffer as-is so we don't need to reallocate it, in case it is // large relative to the size of a line. Instead, we move our cursor // within the buffer forward. $this->num++; $this->line = substr($this->buf, $this->pos, ($endl - $this->pos)); $this->pos = $endl + 1; return; } // We only have part of a line left in the buffer (no delimiter in the // remaining piece), so throw away the part we've already emitted and // continue below. $this->buf = substr($this->buf, $this->pos); $this->pos = 0; } // We weren't able to produce the next line from the bytes we already had // buffered, so read more bytes from the input stream. if ($this->eof) { // NOTE: We keep track of EOF (an empty read) so we don't make any more // reads afterward. Normally, we'll return from the first EOF read, // emit the line, and then next() will be called again. Without tracking // EOF, we'll attempt another read. A well-behaved impelmentation should // still return empty string, but we can protect against any issues // here by keeping a flag. $more = ''; } else { $more = $this->readMore(); } if (strlen($more)) { // We got some bytes, so add them to the buffer and then try again. $this->buf .= $more; continue; } else { // No more bytes. If we have a buffer, return its contents. We // potentially return part of a line here if the last line had no // delimiter, but that currently seems reasonable as a default // behaivor. If we don't have a buffer, we're done. $this->eof = true; if (strlen($this->buf)) { $this->num++; $this->line = $this->buf; $this->buf = null; } else { $this->valid = false; } break; } } } } diff --git a/src/filesystem/linesofalarge/LinesOfALargeExecFuture.php b/src/filesystem/linesofalarge/LinesOfALargeExecFuture.php index 2a0fa3e..1e04d7f 100644 --- a/src/filesystem/linesofalarge/LinesOfALargeExecFuture.php +++ b/src/filesystem/linesofalarge/LinesOfALargeExecFuture.php @@ -1,134 +1,118 @@ future = $future; } /* -( Internals )---------------------------------------------------------- */ /** * On destruction, we terminate the subprocess if it hasn't exited already. * * @return void * @task internals */ public function __destruct() { if (!$this->future->isReady()) { $this->future->resolveKill(); } } /** * The PHP foreach() construct calls rewind() once, so we allow the first * rewind(), without effect. Subsequent rewinds mean misuse. * * @return void * @task internals */ protected function willRewind() { if ($this->didRewind) { throw new Exception( "You can not reiterate over a LinesOfALargeExecFuture object. The ". "entire goal of the construct is to avoid keeping output in memory. ". "What you are attempting to do is silly and doesn't make any sense."); } $this->didRewind = true; } /** * Read more data from the subprocess. * * @return string Bytes read from stdout. * @task internals */ protected function readMore() { $future = $this->future; while (true) { // Read is nonblocking, so we need to sit in this loop waiting for input // or we'll incorrectly signal EOF to the parent. list($stdout) = $future->read(); $future->discardBuffers(); if (strlen($stdout)) { return $stdout; } // If we didn't read anything, we can exit the loop if the subprocess // has exited. if ($future->isReady()) { // Throw if the process exits with a nozero status code. This makes // error handling simpler, and prevents us from returning part of a line // if the process terminates mid-output. $future->resolvex(); // Read and return anything that's left. list($stdout) = $future->read(); $future->discardBuffers(); return $stdout; } } } } diff --git a/src/filesystem/linesofalarge/LinesOfALargeFile.php b/src/filesystem/linesofalarge/LinesOfALargeFile.php index 2a2356a..91b30eb 100644 --- a/src/filesystem/linesofalarge/LinesOfALargeFile.php +++ b/src/filesystem/linesofalarge/LinesOfALargeFile.php @@ -1,124 +1,108 @@ fileName = Filesystem::resolvePath((string)$file_name); } /* -( Internals )---------------------------------------------------------- */ /** * Closes the file handle. * * @return void * @task internals */ public function __destruct() { $this->closeHandle(); } /** * Close the file handle, if it is open. * * @return $this * @task internals */ private function closeHandle() { if ($this->handle) { fclose($this->handle); $this->handle = null; } return $this; } /** * Closes the file handle if it is open, and reopens it. * * @return void * @task internals */ protected function willRewind() { $this->closeHandle(); $this->handle = @fopen($this->fileName, 'r'); if (!$this->handle) { throw new FilesystemException( $this->fileName, "Failed to open file!"); } } /** * Read the file chunk-by-chunk. * * @return string Next chunk of the file. * @task internals */ public function readMore() { // NOTE: At least on OSX in reasonably normal test cases, increasing the // size of this read has no impact on performance. $more = @fread($this->handle, 2048); if ($more === false) { throw new FilesystemException( $this->fileName, "Failed to read file!"); } return $more; } } diff --git a/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php b/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php index 7d5d7e7..e016540 100644 --- a/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php +++ b/src/filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php @@ -1,79 +1,63 @@ writeAndRead( "cat\ndog\nbird\n", array( "cat", "dog", "bird", )); } public function testExecLargeFile() { $line = "The quick brown fox jumps over the lazy dog."; $n = 100; $this->writeAndRead( str_repeat($line."\n", $n), array_fill(0, $n, $line)); } public function testExecLongLine() { $line = str_repeat('x', 64 * 1024); $this->writeAndRead($line, array($line)); } public function testExecException() { $caught = null; try { $future = new ExecFuture('does-not-exist.exe.sh'); foreach (new LinesOfALargeExecFuture($future) as $line) { // ignore } } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof CommandException); } private function writeAndRead($write, $read) { $future = new ExecFuture('cat'); $future->write($write); $lines = array(); foreach (new LinesOfALargeExecFuture($future) as $line) { $lines[] = $line; } $this->assertEqual( $read, $lines, "Write: ".phutil_utf8_shorten($write, 32)); } } diff --git a/src/filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php b/src/filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php index 2827593..105ffa9 100644 --- a/src/filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php +++ b/src/filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php @@ -1,115 +1,99 @@ writeAndRead( "abcd", array( "abcd", )); } public function testTerminalDelimiterPresent() { $this->writeAndRead( "bat\ncat\ndog\n", array( "bat", "cat", "dog", )); } public function testTerminalDelimiterAbsent() { $this->writeAndRead( "bat\ncat\ndog", array( "bat", "cat", "dog", )); } public function testChangeDelimiter() { $this->writeAndRead( "bat\1cat\1dog\1", array( "bat", "cat", "dog", ), "\1"); } public function testEmptyLines() { $this->writeAndRead( "\n\nbat\n", array( '', '', 'bat', )); } public function testLargeFile() { $line = "The quick brown fox jumps over the lazy dog."; $n = 100; $this->writeAndRead( str_repeat($line."\n", $n), array_fill(0, $n, $line)); } public function testLongLine() { $line = str_repeat('x', 64 * 1024); $this->writeAndRead($line, array($line)); } public function testReadFailure() { $caught = null; try { $f = new LinesOfALargeFile('/does/not/exist.void'); $f->rewind(); } catch (FilesystemException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof $ex); } private function writeAndRead($write, $read, $delimiter = "\n") { $tmp = new TempFile(); Filesystem::writeFile($tmp, $write); $lines = array(); $iterator = id(new LinesOfALargeFile($tmp))->setDelimiter($delimiter); foreach ($iterator as $line) { $lines[] = $line; } $this->assertEqual( $read, $lines, "Write: ".phutil_utf8_shorten($write, 32)); } } diff --git a/src/future/Future.php b/src/future/Future.php index da53553..6d81b1b 100644 --- a/src/future/Future.php +++ b/src/future/Future.php @@ -1,207 +1,191 @@ getDefaultWait(); do { $this->checkException(); if ($this->isReady()) { break; } $read = $this->getReadSockets(); $write = $this->getWriteSockets(); if ($timeout !== null) { $elapsed = microtime(true) - $start; if ($elapsed > $timeout) { $this->checkException(); return null; } else { $wait = $timeout - $elapsed; } } if ($read || $write) { self::waitForSockets($read, $write, $wait); } } while (true); $this->checkException(); return $this->getResult(); } public function setException(Exception $ex) { $this->exception = $ex; return $this; } public function getException() { return $this->exception; } /** * If an exception was set by setException(), throw it. */ private function checkException() { if ($this->exception) { throw $this->exception; } } /** * Retrieve a list of sockets which we can wait to become readable while * a future is resolving. If your future has sockets which can be select()ed, * return them here (or in getWriteSockets()) to make the resolve loop do a * select(). If you do not return sockets in either case, you'll get a busy * wait. * * @return list A list of sockets which we expect to become readable. */ public function getReadSockets() { return array(); } /** * Retrieve a list of sockets which we can wait to become writable while a * future is resolving. See getReadSockets(). * * @return list A list of sockets which we expect to become writable. */ public function getWriteSockets() { return array(); } /** * Wait for activity on one of several sockets. * * @param list List of sockets expected to become readable. * @param list List of sockets expected to become writable. * @param float Timeout, in seconds. * @return void */ public static function waitForSockets( array $read_list, array $write_list, $timeout = 1) { if (!self::$handlerInstalled) { // If we're spawning child processes, we need to install a signal handler // here to catch cases like execing '(sleep 60 &) &' where the child // exits but a socket is kept open. But we don't actually need to do // anything because the SIGCHLD will interrupt the stream_select(), as // long as we have a handler registered. if (function_exists('pcntl_signal')) { if (!pcntl_signal(SIGCHLD, array('Future', 'handleSIGCHLD'))) { throw new Exception('Failed to install signal handler!'); } } self::$handlerInstalled = true; } $timeout_sec = (int)$timeout; $timeout_usec = (int)(1000000 * ($timeout - $timeout_sec)); $exceptfds = array(); $ok = @stream_select( $read_list, $write_list, $exceptfds, $timeout, $timeout_usec); if ($ok === false) { // Hopefully, means we received a SIGCHLD. In the worst case, we degrade // to a busy wait. } } public static function handleSIGCHLD($signo) { // This function is a dummy, we just need to have some handler registered // so that PHP will get interrupted during stream_select(). If we don't // register a handler, stream_select() won't fail. } /** * Retrieve the final result of the future. This method will be called after * the future is ready (as per isReady()) but before results are passed back * to the caller. The major use of this function is that you can override it * in subclasses to do postprocessing or error checking, which is * particularly useful if building application-specific futures on top of * primitive transport futures (like CurlFuture and ExecFuture) which can * make it tricky to hook this logic into the main pipeline. * * @return mixed Final resolution of this future. */ protected function getResult() { return $this->result; } /** * Default amount of time to wait on stream select for this future. Normally * 1 second is fine, but if the future has a timeout sooner than that it * should return the amount of time left before the timeout. */ public function getDefaultWait() { return 1; } public function start() { $this->isReady(); return $this; } } diff --git a/src/future/FutureIterator.php b/src/future/FutureIterator.php index 91d69bb..8bbc4dd 100644 --- a/src/future/FutureIterator.php +++ b/src/future/FutureIterator.php @@ -1,341 +1,325 @@ new ExecFuture('wc -c a.txt'), * 'b.txt' => new ExecFuture('wc -c b.txt'), * 'c.txt' => new ExecFuture('wc -c c.txt'), * ); * * foreach (Futures($futures) as $key => $future) { * // IMPORTANT: keys are preserved but the order of elements is not. This * // construct iterates over the futures in the order they resolve, so the * // fastest future is the one you'll get first. This allows you to start * // doing followup processing as soon as possible. * * list($err, $stdout) = $future->resolve(); * do_some_processing($stdout); * } * * For a general overview of futures, see @{article:Using Futures}. * * @task basics Basics * @task config Configuring Iteration * @task iterator Iterator Interface * @task internal Internals * * @group futures */ final class FutureIterator implements Iterator { protected $wait = array(); protected $work = array(); protected $futures = array(); protected $key; protected $limit; protected $timeout; protected $isTimeout = false; /* -( Basics )------------------------------------------------------------- */ /** * Create a new iterator over a list of futures. By convention, use the * convenience function @{function:Futures} instead of instantiating this * class directly. * * @param list List of @{class:Future}s to resolve. * @task basics */ public function __construct(array $futures) { assert_instances_of($futures, 'Future'); $this->futures = $futures; } /** * Block until all futures resolve. * * @return void * @task basics */ public function resolveAll() { foreach ($this as $future) { $future->resolve(); } } /** * Add another future to the set of futures. This is useful if you have a * set of futures to run mostly in parallel, but some futures depend on * others. * * @param Future @{class:Future} to add to iterator * @task basics */ public function addFuture(Future $future, $key = null) { if ($key === null) { $this->futures[] = $future; $this->wait[] = last_key($this->futures); } else if (!isset($this->futures[$key])) { $this->futures[$key] = $future; $this->wait[] = $key; } else { throw new Exception("Invalid key {$key}"); } // Start running the future if we don't have $this->limit futures running // already. updateWorkingSet() won't start running the future if there's no // limit, so we'll manually poke it here in that case. $this->updateWorkingSet(); if (!$this->limit) { $future->isReady(); } return $this; } /* -( Configuring Iteration )---------------------------------------------- */ /** * Set a maximum amount of time you want to wait before the iterator will * yield a result. If no future has resolved yet, the iterator will yield * null for key and value. Among other potential uses, you can use this to * show some busy indicator: * * foreach (Futures($futures)->setUpdateInterval(1) as $future) { * if ($future === null) { * echo "Still working...\n"; * } else { * // ... * } * } * * This will echo "Still working..." once per second as long as futures are * resolving. By default, FutureIterator never yields null. * * @param float Maximum number of seconds to block waiting on futures before * yielding null. * @return this * * @task config */ public function setUpdateInterval($interval) { $this->timeout = $interval; return $this; } /** * Limit the number of simultaneously executing futures. * * foreach (Futures($futures)->limit(4) as $future) { * // Run no more than 4 futures simultaneously. * } * * @param int Maximum number of simultaneous jobs allowed. * @return this * * @task config */ public function limit($max) { $this->limit = $max; return $this; } /* -( Iterator Interface )------------------------------------------------- */ /** * @task iterator */ public function rewind() { $this->wait = array_keys($this->futures); $this->work = null; $this->updateWorkingSet(); $this->next(); } /** * @task iterator */ public function next() { $this->key = null; if (!count($this->wait)) { return; } $read_sockets = array(); $write_sockets = array(); $start = microtime(true); $timeout = $this->timeout; $this->isTimeout = false; $check = $this->getWorkingSet(); $resolve = null; do { $read_sockets = array(); $write_sockets = array(); $can_use_sockets = true; $wait_time = 1; foreach ($check as $wait => $key) { $future = $this->futures[$key]; try { if ($future->getException()) { $resolve = $wait; continue; } if ($future->isReady()) { if ($resolve === null) { $resolve = $wait; } continue; } $got_sockets = false; $socks = $future->getReadSockets(); if ($socks) { $got_sockets = true; foreach ($socks as $socket) { $read_sockets[] = $socket; } } $socks = $future->getWriteSockets(); if ($socks) { $got_sockets = true; foreach ($socks as $socket) { $write_sockets[] = $socket; } } // If any currently active future had neither read nor write sockets, // we can't wait for the current batch of items using sockets. if (!$got_sockets) { $can_use_sockets = false; } else { $wait_time = min($wait_time, $future->getDefaultWait()); } } catch (Exception $ex) { $this->futures[$key]->setException($ex); $resolve = $wait; break; } } if ($resolve === null) { if ($can_use_sockets) { if ($timeout !== null) { $elapsed = microtime(true) - $start; if ($elapsed > $timeout) { $this->isTimeout = true; return; } else { $wait_time = $timeout - $elapsed; } } Future::waitForSockets($read_sockets, $write_sockets, $wait_time); } else { usleep(1000); } } } while ($resolve === null); $this->key = $this->wait[$resolve]; unset($this->wait[$resolve]); $this->updateWorkingSet(); } /** * @task iterator */ public function current() { if ($this->isTimeout) { return null; } return $this->futures[$this->key]; } /** * @task iterator */ public function key() { if ($this->isTimeout) { return null; } return $this->key; } /** * @task iterator */ public function valid() { if ($this->isTimeout) { return true; } return ($this->key !== null); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ protected function getWorkingSet() { if ($this->work === null) { return $this->wait; } return $this->work; } /** * @task internal */ protected function updateWorkingSet() { if (!$this->limit) { return; } $old = $this->work; $this->work = array_slice($this->wait, 0, $this->limit, true); // If we're using a limit, our futures are sleeping and need to be polled // to begin execution, so poll any futures which weren't in our working set // before. foreach ($this->work as $work => $key) { if (!isset($old[$work])) { $this->futures[$key]->isReady(); } } } } diff --git a/src/future/FutureProxy.php b/src/future/FutureProxy.php index 823e7f3..d228e25 100644 --- a/src/future/FutureProxy.php +++ b/src/future/FutureProxy.php @@ -1,90 +1,74 @@ setProxiedFuture($proxied); } } public function setProxiedFuture(Future $proxied) { $this->proxied = $proxied; return $this; } protected function getProxiedFuture() { if (!$this->proxied) { throw new Exception("The proxied future has not been provided yet."); } return $this->proxied; } public function isReady() { return $this->getProxiedFuture()->isReady(); } public function resolve($timeout = null) { $this->getProxiedFuture()->resolve($timeout); return $this->getResult(); } public function setException(Exception $ex) { $this->getProxiedFuture()->setException($ex); return $this; } public function getException() { return $this->getProxiedFuture()->getException(); } public function getReadSockets() { return $this->getProxiedFuture()->getReadSockets(); } public function getWriteSockets() { return $this->getProxiedFuture()->getWriteSockets(); } protected function getResult() { if ($this->result === null) { $result = $this->getProxiedFuture()->resolve(); $result = $this->didReceiveResult($result); $this->result = $result; } return $this->result; } public function start() { $this->getProxiedFuture()->start(); return $this; } abstract protected function didReceiveResult($result); } diff --git a/src/future/ImmediateFuture.php b/src/future/ImmediateFuture.php index fbacc92..bc1a85f 100644 --- a/src/future/ImmediateFuture.php +++ b/src/future/ImmediateFuture.php @@ -1,35 +1,19 @@ result = $result; } public function isReady() { return true; } } diff --git a/src/future/__tests__/FutureIteratorTestCase.php b/src/future/__tests__/FutureIteratorTestCase.php index 77a6d78..18d8e17 100644 --- a/src/future/__tests__/FutureIteratorTestCase.php +++ b/src/future/__tests__/FutureIteratorTestCase.php @@ -1,42 +1,26 @@ limit(2); $results = array(); foreach ($iterator as $future) { if ($future === $future1) { $iterator->addFuture($future2); } $results[] = $future->resolve(); } $this->assertEqual(2, count($results)); } } diff --git a/src/future/aws/PhutilAWSEC2Future.php b/src/future/aws/PhutilAWSEC2Future.php index a94dd07..afa6109 100644 --- a/src/future/aws/PhutilAWSEC2Future.php +++ b/src/future/aws/PhutilAWSEC2Future.php @@ -1,29 +1,13 @@ httpStatus = $http_status; $this->requestID = idx($params, 'RequestID'); $this->params = $params; $desc = array(); $desc[] = 'AWS Request Failed'; $desc[] = 'HTTP Status Code: '.$http_status; if ($this->requestID) { $desc[] = 'AWS Request ID: '.$this->requestID; $errors = idx($params, 'Errors'); if ($errors) { $desc[] = 'AWS Errors:'; foreach ($errors as $error) { list($code, $message) = $error; $desc[] = " - {$code}: {$message}\n"; } } } else { $desc[] = 'Response Body: '.idx($params, 'body'); } $desc = implode("\n", $desc); parent::__construct($desc); } public function getRequestID() { return $this->requestID; } public function getHTTPStatus() { return $this->httpStatus; } } diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php index 47caa15..49ec520 100644 --- a/src/future/aws/PhutilAWSFuture.php +++ b/src/future/aws/PhutilAWSFuture.php @@ -1,149 +1,133 @@ awsAccessKey = $access; $this->awsPrivateKey = $private; return $this; } public function getAWSAccessKey() { return $this->awsAccessKey; } public function getAWSPrivateKey() { return $this->awsPrivateKey; } public function setRawAWSQuery($action, array $params = array()) { $this->params = $params; $this->params['Action'] = $action; return $this; } public function getAWSKeys() { return $this->AWSKeys; } protected function getProxiedFuture() { if (!$this->future) { $params = $this->params; if (!$this->params) { throw new Exception("You must setRawAWSQuery()!"); } if (!$this->getAWSAccessKey()) { throw new Exception("You must setAWSKeys()!"); } $params['AWSAccessKeyId'] = $this->getAWSAccessKey(); $params['Version'] = '2011-12-15'; $params['Timestamp'] = date('c'); $params = $this->sign($params); $uri = new PhutilURI('http://'.$this->getHost().'/'); $uri->setQueryParams($params); $this->future = new HTTPFuture($uri); } return $this->future; } protected function didReceiveResult($result) { list($status, $body, $headers) = $result; try { $xml = @(new SimpleXMLElement($body)); } catch (Exception $ex) { $xml = null; } if ($status->isError() || !$xml) { if (!($status instanceof HTTPFutureResponseStatusHTTP)) { throw $status; } $params = array( 'body' => $body, ); if ($xml) { $params['RequestID'] = $xml->RequestID[0]; foreach ($xml->Errors[0] as $error) { $params['Errors'][] = array($error->Code, $error->Message); } } throw new PhutilAWSException($status->getStatusCode(), $params); } return $xml; } /** * http://bit.ly/wU0JFh */ private function sign(array $params) { $params['SignatureMethod'] = 'HmacSHA256'; $params['SignatureVersion'] = '2'; ksort($params); $pstr = array(); foreach ($params as $key => $value) { $pstr[] = rawurlencode($key).'='.rawurlencode($value); } $pstr = implode('&', $pstr); $sign = "GET"."\n". strtolower($this->getHost())."\n". "/"."\n". $pstr; $hash = hash_hmac( 'sha256', $sign, $this->getAWSPrivateKey(), $raw_ouput = true); $params['Signature'] = base64_encode($hash); return $params; } } diff --git a/src/future/exec/CommandException.php b/src/future/exec/CommandException.php index b24ebee..e37691d 100644 --- a/src/future/exec/CommandException.php +++ b/src/future/exec/CommandException.php @@ -1,54 +1,38 @@ command = $command; $this->error = $error; $this->stdout = $stdout; $this->stderr = $stderr; } public function getCommand() { return $this->command; } public function getError() { return $this->error; } public function getStdout() { return $this->stdout; } public function getStderr() { return $this->stderr; } } diff --git a/src/future/exec/ExecFuture.php b/src/future/exec/ExecFuture.php index 503f400..97c44cd 100644 --- a/src/future/exec/ExecFuture.php +++ b/src/future/exec/ExecFuture.php @@ -1,692 +1,676 @@ array('pipe', 'r'), // stdin 1 => array('pipe', 'w'), // stdout 2 => array('pipe', 'w'), // stderr ); /* -( Creating ExecFutures )----------------------------------------------- */ /** * Create a new ExecFuture. * * $future = new ExecFuture('wc -l %s', $file_path); * * @param string ##sprintf()##-style command string which will be passed * through @{function:csprintf} with the rest of the arguments. * @param ... Zero or more additional arguments for @{function:csprintf}. * @return ExecFuture ExecFuture for running the specified command. * @task create */ public function __construct($command) { $argv = func_get_args(); $this->command = call_user_func_array('csprintf', $argv); } /* -( Command Information )------------------------------------------------ */ /** * Retrieve the raw command to be executed. * * @return string Raw command. * @task info */ public function getCommand() { return $this->command; } /** * Retrieve the byte limit for the stderr buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStderrSizeLimit() { return $this->stderrSizeLimit; } /** * Retrieve the byte limit for the stdout buffer. * * @return int Maximum buffer size, in bytes. * @task info */ public function getStdoutSizeLimit() { return $this->stdoutSizeLimit; } /** * Get the process's pid. This only works after execution is initiated, e.g. * by a call to start(). * * @return int Process ID of the executing process. * @task info */ public function getPID() { $status = $this->procGetStatus(); return $status['pid']; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a maximum size for the stdout read buffer. To limit stderr, see * @{method:setStderrSizeLimit}. The major use of these methods is to use less * memory if you are running a command which sometimes produces huge volumes * of output that you don't really care about. * * NOTE: Setting this to 0 means "no buffer", not "unlimited buffer". * * @param int Maximum size of the stdout read buffer. * @return this * @task config */ public function setStdoutSizeLimit($limit) { $this->stdoutSizeLimit = $limit; return $this; } /** * Set a maximum size for the stderr read buffer. * See @{method:setStdoutSizeLimit} for discussion. * * @param int Maximum size of the stderr read buffer. * @return this * @task config */ public function setStderrSizeLimit($limit) { $this->stderrSizeLimit = $limit; return $this; } /** * Set the current working directory to use when executing the command. * * @param string Directory to set as CWD before executing the command. * @return this * @task config */ public function setCWD($cwd) { $this->cwd = $cwd; return $this; } /* -( Interacting With Commands )------------------------------------------ */ /** * Read and return output from stdout and stderr, if any is available. This * method keeps a read cursor on each stream, but the entire streams are * still returned when the future resolves. You can call read() again after * resolving the future to retrieve only the parts of the streams you did not * previously read: * * $future = new ExecFuture('...'); * // ... * list($stdout) = $future->read(); // Returns output so far * list($stdout) = $future->read(); // Returns new output since first call * // ... * list($stdout) = $future->resolvex(); // Returns ALL output * list($stdout) = $future->read(); // Returns unread output * * NOTE: If you set a limit with @{method:setStdoutSizeLimit} or * @{method:setStderrSizeLimit}, this method will not be able to read data * past the limit. * * NOTE: If you call @{method:discardBuffers}, all the stdout/stderr data * will be thrown away and the cursors will be reset. * * @return pair <$stdout, $stderr> pair with new output since the last call * to this method. * @task interact */ public function read() { if ($this->start) { $this->isReady(); // Sync } $result = array( (string)substr($this->stdout, $this->stdoutPos), (string)substr($this->stderr, $this->stderrPos), ); $this->stdoutPos = strlen($this->stdout); $this->stderrPos = strlen($this->stderr); return $result; } /** * Write data to stdin of the command. * * @param string Data to write. * @param bool If true, keep the pipe open for writing. By default, the pipe * will be closed as soon as possible so that commands which * listen for EOF will execute. If you want to keep the pipe open * past the start of command execution, do an empty write with * `$keep_pipe = true` first. * @return this * @task interact */ public function write($data, $keep_pipe = false) { $this->stdin .= $data; $this->closePipe = !$keep_pipe; return $this; } /** * Permanently discard the stdout and stderr buffers and reset the read * cursors. This is basically useful only if you are streaming a large amount * of data from some process: * * $future = new ExecFuture('zcat huge_file.gz'); * do { * $done = $future->resolve(0.1); // Every 100ms, * list($stdout) = $future->read(); // read output... * echo $stdout; // send it somewhere... * $future->discardBuffers(); // and then free the buffers. * } while ($done === null); * * Conceivably you might also need to do this if you're writing a client using * ExecFuture and ##netcat##, but you probably should not do that. * * NOTE: This completely discards the data. It won't be available when the * future resolves. This is almost certainly only useful if you need the * buffer memory for some reason. * * @return this * @task interact */ public function discardBuffers() { $this->stdout = ''; $this->stderr = ''; $this->stdoutPos = 0; $this->stderrPos = 0; return $this; } /** * Returns true if this future was killed by a timeout configured with * @{method:setTimeout}. * * @return bool True if the future was killed for exceeding its time limit. */ public function getWasKilledByTimeout() { return $this->killedByTimeout; } /* -( Configuring Execution )---------------------------------------------- */ /** * Set a hard limit on execution time. If the command runs longer, it will * be killed and the future will resolve with an error code. You can test * if a future was killed by a timeout with @{method:getWasKilledByTimeout}. * * @param int Maximum number of seconds this command may execute for. * @return this * @task config */ public function setTimeout($seconds) { $this->timeout = $seconds; return $this; } /* -( Resolving Execution )------------------------------------------------ */ /** * Resolve a command you expect to exit with return code 0. Works like * @{method:resolve}, but throws if $err is nonempty. Returns only * $stdout and $stderr. See also @{function:execx}. * * list($stdout, $stderr) = $future->resolvex(); * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return pair <$stdout, $stderr> pair. * @task resolve */ public function resolvex($timeout = null) { list($err, $stdout, $stderr) = $this->resolve($timeout); if ($err) { $cmd = $this->command; throw new CommandException( "Command '{$cmd}' failed with error #{$err}:\n". "stdout:\n{$stdout}\n". "stderr:\n{$stderr}\n", $cmd, $err, $stdout, $stderr); } return array($stdout, $stderr); } /** * Resolve a command you expect to return valid JSON. Works like * @{method:resolvex}, but also throws if stderr is nonempty, or stdout is not * valid JSON. Returns a PHP array, decoded from the JSON command output. * * @param float Optional timeout after which resolution will pause and * execution will return to the caller. * @return array PHP array, decoded from JSON command output. * @task resolve */ public function resolveJSON($timeout = null) { list($stdout, $stderr) = $this->resolvex($timeout); if (strlen($stderr)) { $cmd = $this->command; throw new CommandException( "JSON command '{$cmd}' emitted text to stderr when none was expected: ". $stderr, $cmd, 0, $stdout, $stderr); } $object = json_decode($stdout, true); if (!is_array($object)) { $cmd = $this->command; throw new CommandException( "JSON command '{$cmd}' did not produce a valid JSON object on stdout: ". $stdout, $cmd, 0, $stdout, $stderr); } return $object; } /** * Resolve the process by abruptly terminating it. * * @return list List of results. * @task resolve */ public function resolveKill() { if (defined('SIGKILL')) { $signal = SIGKILL; } else { $signal = 9; } proc_terminate($this->proc, $signal); $this->result = array( 128 + $signal, $this->stdout, $this->stderr); $this->__destruct(); $this->endProfile(); return $this->result; } /* -( Internals )---------------------------------------------------------- */ /** * Provides read sockets to the future core. * * @return list List of read sockets. * @task internal */ public function getReadSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdout) && !feof($stdout)) { $sockets[] = $stdout; } if (isset($stderr) && !feof($stderr)) { $sockets[] = $stderr; } return $sockets; } /** * Provides write sockets to the future core. * * @return list List of write sockets. * @task internal */ public function getWriteSockets() { list($stdin, $stdout, $stderr) = $this->pipes; $sockets = array(); if (isset($stdin) && strlen($this->stdin) && !feof($stdin)) { $sockets[] = $stdin; } return $sockets; } /** * Reads some bytes from a stream, discarding output once a certain amount * has been accumulated. * * @param resource Stream to read from. * @param int Maximum number of bytes to return from $stream. If * additional bytes are available, they will be read and * discarded. * @param string Human-readable description of stream, for exception * message. * @return string The data read from the stream. * @task internal */ protected function readAndDiscard($stream, $limit, $description) { $output = ''; do { $data = fread($stream, 4096); if (false === $data) { throw new Exception('Failed to read from '.$description); } $read_bytes = strlen($data); if ($read_bytes > 0 && $limit > 0) { if ($read_bytes > $limit) { $data = substr($data, 0, $limit); } $output .= $data; $limit -= strlen($data); } } while ($read_bytes > 0); return $output; } /** * Begin or continue command execution. * * @return bool True if future has resolved. * @task internal */ public function isReady() { if (!$this->pipes) { $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'exec', 'command' => $this->command, )); if (!$this->start) { // We might already have started the timer via initating resolution. $this->start = microtime(true); } $pipes = array(); $proc = proc_open( $this->command, self::$descriptorSpec, $pipes, $this->cwd); if (!is_resource($proc)) { throw new Exception('Failed to open process.'); } $this->pipes = $pipes; $this->proc = $proc; list($stdin, $stdout, $stderr) = $pipes; if (!phutil_is_windows()) { // On Windows, there's no such thing as nonblocking interprocess I/O. // Just leave the sockets blocking and hope for the best. Some features // will not work. if ((!stream_set_blocking($stdout, false)) || (!stream_set_blocking($stderr, false)) || (!stream_set_blocking($stdin, false))) { $this->__destruct(); throw new Exception('Failed to set streams nonblocking.'); } } $this->tryToCloseStdin(); return false; } if (!$this->proc) { return true; } list($stdin, $stdout, $stderr) = $this->pipes; if (isset($this->stdin) && strlen($this->stdin)) { $bytes = fwrite($stdin, $this->stdin); if ($bytes === false) { throw new Exception('Unable to write to stdin!'); } else if ($bytes) { $this->stdin = substr($this->stdin, $bytes); } } $this->tryToCloseStdin(); // Read status before reading pipes so that we can never miss data that // arrives between our last read and the process exiting. $status = $this->procGetStatus(); $this->stdout .= $this->readAndDiscard( $stdout, $this->getStdoutSizeLimit() - strlen($this->stdout), 'stdout'); $this->stderr .= $this->readAndDiscard( $stderr, $this->getStderrSizeLimit() - strlen($this->stderr), 'stderr'); if (!$status['running']) { $this->result = array( $status['exitcode'], $this->stdout, $this->stderr, ); $this->__destruct(); $this->endProfile(); return true; } $elapsed = (microtime(true) - $this->start); if ($this->timeout && ($elapsed >= $this->timeout)) { $this->killedByTimeout = true; $this->resolveKill(); return true; } } /** * Close and free resources if necessary. * * @return void * @task internal */ public function __destruct() { foreach ($this->pipes as $pipe) { if (isset($pipe)) { @fclose($pipe); } } $this->pipes = array(null, null, null); if ($this->proc) { @proc_close($this->proc); $this->proc = null; } $this->stdin = null; } /** * End the service call profiler for this command. * * @return void * @task internal */ private function endProfile() { if ($this->profilerCallID !== null) { $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall( $this->profilerCallID, array( 'err' => $this->result ? idx($this->result, 0) : null, )); } } /** * Execute proc_get_status(), but avoid pitfalls. * * @return dict Process status. * @task internal */ private function procGetStatus() { // After the process exits, we only get one chance to read proc_get_status() // before it starts returning garbage. Make sure we don't throw away the // last good read. if ($this->procStatus) { if (!$this->procStatus['running']) { return $this->procStatus; } } $this->procStatus = proc_get_status($this->proc); return $this->procStatus; } /** * Try to close stdin, if we're done using it. This keeps us from hanging if * the process on the other end of the pipe is waiting for EOF. * * @return void * @task internal */ private function tryToCloseStdin() { if (!$this->closePipe) { // We've been told to keep the pipe open by a call to write(..., true). return; } if (strlen($this->stdin)) { // We still have bytes to write. return; } list($stdin) = $this->pipes; if (!$stdin) { // We've already closed stdin. return; } // There's nothing stopping us from closing stdin, so close it. @fclose($stdin); $this->pipes[0] = null; } public function getDefaultWait() { $wait = parent::getDefaultWait(); if ($this->timeout) { if (!$this->start) { $this->start = microtime(true); } $elapsed = (microtime(true) - $this->start); $wait = max(0, min($this->timeout - $elapsed, $wait)); } return $wait; } } diff --git a/src/future/exec/__tests__/ExecFutureTestCase.php b/src/future/exec/__tests__/ExecFutureTestCase.php index 7f797fc..0b88f75 100644 --- a/src/future/exec/__tests__/ExecFutureTestCase.php +++ b/src/future/exec/__tests__/ExecFutureTestCase.php @@ -1,116 +1,100 @@ write('')->resolvex(); $this->assertEqual('', $stdout); } public function testKeepPipe() { // NOTE: This is mosty testing the semantics of $keep_pipe in write(). list($stdout) = id(new ExecFuture('cat')) ->write('', true) ->start() ->write('x', true) ->write('y', true) ->write('z', false) ->resolvex(); $this->assertEqual('xyz', $stdout); } public function testLargeBuffer() { // NOTE: This is mostly a coverage test to hit branches where we're still // flushing a buffer. $data = str_repeat('x', 1024 * 1024 * 4); list($stdout) = id(new ExecFuture('cat'))->write($data)->resolvex(); $this->assertEqual($data, $stdout); } public function testBufferLimit() { $data = str_repeat('x', 1024 * 1024); list($stdout) = id(new ExecFuture('cat')) ->setStdoutSizeLimit(1024) ->write($data) ->resolvex(); $this->assertEqual(substr($data, 0, 1024), $stdout); } public function testResolveTimeoutTestShouldRunLessThan1Sec() { // NOTE: This tests interactions between the resolve() timeout and the // ExecFuture timeout, which are similar but not identical. $future = id(new ExecFuture('sleep 32000'))->start(); $future->setTimeout(32000); // We expect this to return in 0.01s. $result = $future->resolve(0.01); $this->assertEqual($result, null); // We expect this to now force the time out / kill immediately. If we don't // do this, we'll hang when exiting until our subprocess exits (32000 // seconds!) $future->setTimeout(0.01); $future->resolve(); } public function testTimeoutTestShouldRunLessThan1Sec() { // NOTE: This is partly testing that we choose appropriate select wait // times; this test should run for significantly less than 1 second. $future = new ExecFuture('sleep 32000'); list($err) = $future->setTimeout(0.01)->resolve(); $this->assertEqual(true, $err > 0); $this->assertEqual(true, $future->getWasKilledByTimeout()); } public function testMultipleTimeoutsTestShouldRunLessThan1Sec() { $futures = array(); for ($ii = 0; $ii < 4; $ii++) { $futures[] = id(new ExecFuture('sleep 32000'))->setTimeout(0.01); } foreach (Futures($futures) as $future) { list ($err) = $future->resolve(); $this->assertEqual(true, $err > 0); $this->assertEqual(true, $future->getWasKilledByTimeout()); } } } diff --git a/src/future/exec/execx.php b/src/future/exec/execx.php index 5e09f08..b22d8e5 100644 --- a/src/future/exec/execx.php +++ b/src/future/exec/execx.php @@ -1,109 +1,93 @@ resolvex(); } /** * Execute a command and capture stdout, stderr, and the return value. * * list ($err, $stdout, $stderr) = exec_manual('ls %s', $file); * * When invoking this function, you must manually handle the error * condition. Error flows can often be simplified by using @{function:execx} * instead, which throws an exception when it encounters an error. * * @param string sprintf()-style command pattern to execute. * @param ... Arguments to sprintf pattern. * @return array List of return code, stdout, and stderr. * @group exec */ function exec_manual($cmd /* , ... */) { $args = func_get_args(); $ef = newv('ExecFuture', $args); return $ef->resolve(); } /** * Execute a command which takes over stdin, stdout and stderr, similar to * passthru(), but which preserves TTY semantics, escapes arguments, and is * traceable. * * @param string sprintf()-style command pattern to execute. * @param ... Arguments to sprintf pattern. * @return int Return code. * @group exec */ function phutil_passthru($cmd /* , ... */) { $args = func_get_args(); $command = call_user_func_array('csprintf', $args); $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'exec', 'subtype' => 'passthru', 'command' => $command, )); $spec = array(STDIN, STDOUT, STDERR); $pipes = array(); if (phutil_is_windows()) { // Without 'bypass_shell', things like launching vim don't work properly, // and we can't execute commands with spaces in them, and all commands // invoked from git bash fail horridly, and everything is a mess in general. $options = array( 'bypass_shell' => true, ); $proc = @proc_open($command, $spec, $pipes, null, null, $options); } else { $proc = @proc_open($command, $spec, $pipes); } if ($proc === false) { $err = 1; } else { $err = proc_close($proc); } $profiler->endServiceCall( $call_id, array( 'err' => $err, )); return $err; } diff --git a/src/future/functions.php b/src/future/functions.php index fa6598c..adbb00b 100644 --- a/src/future/functions.php +++ b/src/future/functions.php @@ -1,28 +1,12 @@ resolve(); * * This is an abstract base class which defines the API that HTTP futures * conform to. Concrete implementations are available in @{class:HTTPFuture} * and @{class:HTTPSFuture}. All futures return a tuple * when resolved; status is an object of class @{class:HTTPFutureResponseStatus} * and may represent any of a wide variety of errors in the transport layer, * a support library, or the actual HTTP exchange. * * @task create Creating a New Request * @task config Configuring the Request * @task resolve Resolving the Request * @task internal Internals * @group futures */ abstract class BaseHTTPFuture extends Future { private $method = 'GET'; private $timeout = 300.0; private $headers = array(); private $uri; private $data; /* -( Creating a New Request )--------------------------------------------- */ /** * Build a new future which will make an HTTP request to a given URI, with * some optional data payload. Since this class is abstract you can't actually * instantiate it; instead, build a new @{class:HTTPFuture} or * @{class:HTTPSFuture}. * * @param string Fully-qualified URI to send a request to. * @param mixed String or array to include in the request. Strings will be * transmitted raw; arrays will be encoded and sent as * 'application/x-www-form-urlencoded'. * @task create */ final public function __construct($uri, $data = array()) { $this->setURI((string)$uri); $this->setData($data); } /* -( Configuring the Request )-------------------------------------------- */ /** * Set a timeout for the service call. If the request hasn't resolved yet, * the future will resolve with a status that indicates the request timed * out. You can determine if a status is a timeout status by calling * isTimeout() on the status object. * * @param float Maximum timeout, in seconds. * @return this * @task config */ public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } /** * Get the currently configured timeout. * * @return float Maximum number of seconds the request will execute for. * @task config */ public function getTimeout() { return $this->timeout; } /** * Select the HTTP method (e.g., "GET", "POST", "PUT") to use for the request. * By default, requests use "GET". * * @param string HTTP method name. * @return this * @task config */ final public function setMethod($method) { static $supported_methods = array( 'GET' => true, 'POST' => true, 'PUT' => true, ); if (empty($supported_methods[$method])) { $method_list = implode(', ', array_keys($supported_methods)); throw new Exception( "The HTTP method '{$method}' is not supported. Supported HTTP methods ". "are: {$method_list}."); } $this->method = $method; return $this; } /** * Get the HTTP method the request will use. * * @return string HTTP method name, like "GET". * @task config */ final public function getMethod() { return $this->method; } /** * Set the URI to send the request to. Note that this is also a constructor * parameter. * * @param string URI to send the request to. * @return this * @task config */ public function setURI($uri) { $this->uri = (string)$uri; return $this; } /** * Get the fully-qualified URI the request will be made to. * * @return string URI the request will be sent to. * @task config */ public function getURI() { return $this->uri; } /** * Provide data to send along with the request. Note that this is also a * constructor parameter; it may be more convenient to provide it there. Data * must be a string (in which case it will be sent raw) or an array (in which * case it will be encoded and sent as 'application/x-www-form-urlencoded'). * * @param mixed Data to send with the request. * @return this * @task config */ public function setData($data) { if (!is_string($data) && !is_array($data)) { throw new Exception("Data parameter must be an array or string."); } $this->data = $data; return $this; } /** * Get the data which will be sent with the request. * * @return mixed Data which will be sent. * @task config */ public function getData() { return $this->data; } /** * Add an HTTP header to the request. The same header name can be specified * more than once, which will cause multiple headers to be sent. * * @param string Header name, like "Accept-Language". * @param string Header value, like "en-us". * @return this * @task config */ public function addHeader($name, $value) { $this->headers[] = array($name, $value); return $this; } /** * Get headers which will be sent with the request. Optionally, you can * provide a filter, which will return only headers with that name. For * example: * * $all_headers = $future->getHeaders(); * $just_user_agent = $future->getHeaders('User-Agent'); * * In either case, an array with all (or all matching) headers is returned. * * @param string|null Optional filter, which selects only headers with that * name if provided. * @return array List of all (or all matching) headers. * @task config */ public function getHeaders($filter = null) { $filter = strtolower($filter); $result = array(); foreach ($this->headers as $header) { list($name, $value) = $header; if (!$filter || ($filter == strtolower($name))) { $result[] = $header; } } return $result; } /* -( Resolving the Request )---------------------------------------------- */ /** * Exception-oriented resolve(). Throws if the status indicates an error * occurred. * * @return tuple HTTP request result tuple. * @task resolve */ final public function resolvex() { $result = $this->resolve(); list($status, $body, $headers) = $result; if ($status->isError()) { throw $status; } return array($body, $headers); } /* -( Internals )---------------------------------------------------------- */ /** * Parse a raw HTTP response into a tuple. * * @param string Raw HTTP response. * @return tuple Valid resolution tuple. * @task internal */ protected function parseRawHTTPResponse($raw_response) { $rex_base = "@^(?P.*?)\r?\n\r?\n(?P.*)$@s"; $rex_head = "@^HTTP/\S+ (?P\d+) (?P.*?)". "(?:\r?\n(?P.*))?$@s"; // We need to parse one or more header blocks in case we got any // "HTTP/1.X 100 Continue" nonsense back as part of the response. This // happens with HTTPS requests, at the least. $response = $raw_response; while (true) { $matches = null; if (!preg_match($rex_base, $response, $matches)) { return $this->buildMalformedResult($raw_response); } $head = $matches['head']; $body = $matches['body']; if (!preg_match($rex_head, $head, $matches)) { return $this->buildMalformedResult($raw_response); } $response_code = (int)$matches['code']; $response_status = strtolower($matches['status']); if ($response_code == 100) { // This is HTTP/1.X 100 Continue, so this whole chunk is moot. $response = $body; } else if (($response_code == 200) && ($response_status == 'connection established')) { // When tunneling through an HTTPS proxy, we get an initial header // block like "HTTP/1.X 200 Connection established", then newlines, // then the normal response. Drop this chunk. $response = $body; } else { $headers = $this->parseHeaders(idx($matches, 'headers')); break; } } $status = new HTTPFutureResponseStatusHTTP($response_code, $body); return array($status, $body, $headers); } /** * Parse an HTTP header block. * * @param string Raw HTTP headers. * @return list List of HTTP header tuples. * @task internal */ protected function parseHeaders($head_raw) { $rex_header = '@^(?P.*?):\s*(?P.*)$@'; $headers = array(); if (!$head_raw) { return $headers; } $headers_raw = preg_split("/\r?\n/", $head_raw); foreach ($headers_raw as $header) { $m = null; if (preg_match($rex_header, $header, $m)) { $headers[] = array($m['name'], $m['value']); } else { $headers[] = array($header, null); } } return $headers; } /** * Build a result tuple indicating a parse error resulting from a malformed * HTTP response. * * @return tuple Valid resolution tuple. * @task internal */ protected function buildMalformedResult($raw_response) { $body = null; $headers = array(); $status = new HTTPFutureResponseStatusParse( HTTPFutureResponseStatusParse::ERROR_MALFORMED_RESPONSE, $raw_response); return array($status, $body, $headers); } } diff --git a/src/future/http/HTTPFuture.php b/src/future/http/HTTPFuture.php index aa8a855..a63dab3 100644 --- a/src/future/http/HTTPFuture.php +++ b/src/future/http/HTTPFuture.php @@ -1,314 +1,298 @@ resolvex(); * * Or * * $future = new HTTPFuture('http://www.example.com/'); * list($http_response_status_object, * $response_body, * $headers) = $future->resolve(); * * Prefer resolvex() to resolve() as the former throws * @{class:HTTPFutureResponseStatusHTTP} on failures, which includes an * informative exception message. * * @group futures */ final class HTTPFuture extends BaseHTTPFuture { private $host; private $port = 80; private $fullRequestPath; private $socket; private $writeBuffer; private $response; private $stateConnected = false; private $stateWriteComplete = false; private $stateReady = false; private $stateStartTime; private $profilerCallID; public function setURI($uri) { $parts = parse_url($uri); if (!$parts) { throw new Exception("Could not parse URI '{$uri}'."); } if (empty($parts['scheme']) || $parts['scheme'] !== 'http') { throw new Exception( "URI '{$uri}' must be fully qualified with 'http://' scheme."); } if (!isset($parts['host'])) { throw new Exception( "URI '{$uri}' must be fully qualified and include host name."); } $this->host = $parts['host']; if (!empty($parts['port'])) { $this->port = $parts['port']; } if (isset($parts['user']) || isset($parts['pass'])) { throw new Exception( "HTTP Basic Auth is not supported by HTTPFuture."); } if (isset($parts['path'])) { $this->fullRequestPath = $parts['path']; } else { $this->fullRequestPath = '/'; } if (isset($parts['query'])) { $this->fullRequestPath .= '?'.$parts['query']; } return parent::setURI($uri); } public function __destruct() { if ($this->socket) { @fclose($this->socket); $this->socket = null; } } public function getReadSockets() { if ($this->socket) { return array($this->socket); } return array(); } public function getWriteSockets() { if (strlen($this->writeBuffer)) { return array($this->socket); } return array(); } public function isWriteComplete() { return $this->stateWriteComplete; } private function getDefaultUserAgent() { return 'HTTPFuture/1.0'; } public function isReady() { if ($this->stateReady) { return true; } if (!$this->socket) { $this->stateStartTime = microtime(true); $this->socket = $this->buildSocket(); if (!$this->socket) { return $this->stateReady; } $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'http', 'uri' => $this->getURI(), )); } if (!$this->stateConnected) { $read = array(); $write = array($this->socket); $except = array(); $select = stream_select($read, $write, $except, $tv_sec = 0); if ($write) { $this->stateConnected = true; } } if ($this->stateConnected) { if (strlen($this->writeBuffer)) { $bytes = @fwrite($this->socket, $this->writeBuffer); if ($bytes === false) { throw new Exception("Failed to write to buffer."); } else if ($bytes) { $this->writeBuffer = substr($this->writeBuffer, $bytes); } } if (!strlen($this->writeBuffer)) { $this->stateWriteComplete = true; } while (($data = fread($this->socket, 32768)) || strlen($data)) { $this->response .= $data; } if ($data === false) { throw new Exception("Failed to read socket."); } } return $this->checkSocket(); } private function buildSocket() { $errno = null; $errstr = null; $socket = @stream_socket_client( 'tcp://'.$this->host.':'.$this->port, $errno, $errstr, $ignored_connection_timeout = 1.0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if (!$socket) { $this->stateReady = true; $this->result = $this->buildErrorResult( HTTPFutureResponseStatusTransport::ERROR_CONNECTION_FAILED); return null; } $ok = stream_set_blocking($socket, 0); if (!$ok) { throw new Exception("Failed to set stream nonblocking."); } $this->writeBuffer = $this->buildHTTPRequest(); return $socket; } private function checkSocket() { $timeout = false; $now = microtime(true); if (($now - $this->stateStartTime) > $this->getTimeout()) { $timeout = true; } if (!feof($this->socket) && !$timeout) { return false; } $this->stateReady = true; if ($timeout) { $this->result = $this->buildErrorResult( HTTPFutureResponseStatusTransport::ERROR_TIMEOUT); } else if (!$this->stateConnected) { $this->result = $this->buildErrorResult( HTTPFutureResponseStatusTransport::ERROR_CONNECTION_REFUSED); } else if (!$this->stateWriteComplete) { $this->result = $this->buildErrorResult( HTTPFutureResponseStatusTransport::ERROR_CONNECTION_FAILED); } else { $this->result = $this->parseRawHTTPResponse($this->response); } $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($this->profilerCallID, array()); return true; } private function buildErrorResult($error) { return array( $status = new HTTPFutureResponseStatusTransport($error, $this->getURI()), $body = null, $headers = array()); } private function buildHTTPRequest() { $data = $this->getData(); $method = $this->getMethod(); $uri = $this->fullRequestPath; $add_headers = array(); if ($this->getMethod() == 'GET') { if (is_array($data)) { $data = http_build_query($data, '', '&'); if (strpos($uri, '?') !== false) { $uri .= '&'.$data; } else { $uri .= '?'.$data; } $data = ''; } } else { if (is_array($data)) { $data = http_build_query($data, '', '&')."\r\n"; $add_headers[] = array( 'Content-Type', 'application/x-www-form-urlencoded'); } } $length = strlen($data); $add_headers[] = array( 'Content-Length', $length); if (!$this->getHeaders('User-Agent')) { $add_headers[] = array( 'User-Agent', $this->getDefaultUserAgent()); } if (!$this->getHeaders('Host')) { $add_headers[] = array( 'Host', $this->host); } $headers = array_merge($this->getHeaders(), $add_headers); foreach ($headers as $key => $header) { list($name, $value) = $header; if (strlen($value)) { $value = ': '.$value; } $headers[$key] = $name.$value."\r\n"; } return "{$method} {$uri} HTTP/1.0\r\n". implode('', $headers). "\r\n". $data; } } diff --git a/src/future/http/HTTPSFuture.php b/src/future/http/HTTPSFuture.php index 848329a..c312f25 100644 --- a/src/future/http/HTTPSFuture.php +++ b/src/future/http/HTTPSFuture.php @@ -1,178 +1,162 @@ setTimeout($timeout); } try { list($body) = $future->resolvex(); return $body; } catch (HTTPFutureResponseStatus $ex) { return false; } } public function isReady() { if (isset($this->result)) { return true; } $uri = $this->getURI(); $data = $this->getData(); if ($data) { // NOTE: PHP's cURL implementation has a piece of magic which treats // parameters as file paths if they begin with '@'. This means that // an array like "array('name' => '@/usr/local/secret')" will attempt to // read that file off disk and send it to the remote server. This behavior // is pretty surprising, and it can easily become a relatively severe // security vulnerability which allows an attacker to read any file the // HTTP process has access to. Since this feature is very dangerous and // not particularly useful, we prevent its use. // // After PHP 5.2.0, it is sufficient to pass a string to avoid this // "feature" (it is only invoked in the array version). Prior to // PHP 5.2.0, we block any request which have string data beginning with // '@' (they would not work anyway). if (is_array($data)) { // Explicitly build a query string to prevent "@" security problems. $data = http_build_query($data, '', '&'); } if ($data[0] == '@' && version_compare(phpversion(), '5.2.0', '<')) { throw new Exception( "Attempting to make an HTTP request including string data that ". "begins with '@'. Prior to PHP 5.2.0, this reads files off disk, ". "which creates a wide attack window for security vulnerabilities. ". "Upgrade PHP or avoid making cURL requests which begin with '@'."); } } else { $data = null; } $profiler = PhutilServiceProfiler::getInstance(); $this->profilerCallID = $profiler->beginServiceCall( array( 'type' => 'http', 'uri' => $uri, )); // NOTE: If possible, we reuse the handle so we can take advantage of // keepalives. This means every option has to be set every time, because // cURL will not clear the settings between requests. if (!self::$handle) { self::$handle = curl_init(); } $curl = self::$handle; curl_setopt($curl, CURLOPT_URL, $uri); curl_setopt($curl, CURLOPT_POSTFIELDS, $data); $headers = $this->getHeaders(); if ($headers) { for ($ii = 0; $ii < count($headers); $ii++) { list($name, $value) = $headers[$ii]; $headers[$ii] = $name.': '.$value; } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); } else { curl_setopt($curl, CURLOPT_HTTPHEADER, array()); } // Set the requested HTTP method, e.g. GET / POST / PUT. curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->getMethod()); // Make sure we get the headers and data back. curl_setopt($curl, CURLOPT_HEADER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_MAXREDIRS, 20); if (defined('CURLOPT_TIMEOUT_MS')) { // If CURLOPT_TIMEOUT_MS is available, use the higher-precision timeout. $timeout = max(1, ceil(1000 * $this->getTimeout())); curl_setopt($curl, CURLOPT_TIMEOUT_MS, $timeout); } else { // Otherwise, fall back to the lower-precision timeout. $timeout = max(1, ceil($this->getTimeout())); curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); } $ini_val = ini_get('curl.cainfo'); if (!$ini_val) { $caroot = dirname(phutil_get_library_root('phutil')).'/resources/ssl/'; if (Filesystem::pathExists($caroot.'custom.pem')) { $cabundle = $caroot.'custom.pem'; } else { $cabundle = $caroot.'default.pem'; } curl_setopt($curl, CURLOPT_CAINFO, $cabundle); } curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($curl, CURLOPT_SSLVERSION, 3); $result = curl_exec($curl); $err_code = curl_errno($curl); if ($err_code) { $status = new HTTPFutureResponseStatusCURL($err_code, $uri); $body = null; $headers = array(); $this->result = array($status, $body, $headers); } else { // cURL returns headers of all redirects, we strip all but the final one. $redirects = curl_getinfo($curl, CURLINFO_REDIRECT_COUNT); $result = preg_replace('/^(.*\r\n\r\n){'.$redirects.'}/sU', '', $result); $this->result = $this->parseRawHTTPResponse($result); } // NOTE: Don't call curl_close(), we want to use keepalive if possible. $profiler = PhutilServiceProfiler::getInstance(); $profiler->endServiceCall($this->profilerCallID, array()); return true; } } diff --git a/src/future/http/status/HTTPFutureResponseStatus.php b/src/future/http/status/HTTPFutureResponseStatus.php index e530979..f17a770 100644 --- a/src/future/http/status/HTTPFutureResponseStatus.php +++ b/src/future/http/status/HTTPFutureResponseStatus.php @@ -1,60 +1,44 @@ statusCode = $status_code; $this->uri = (string)$uri; $type = $this->getErrorCodeType($status_code); $description = $this->getErrorCodeDescription($status_code); $uri_info = ''; if ($this->uri) { $uri_info = ' ('.$this->uri.')'; } $message = rtrim("[{$type}/{$status_code}]{$uri_info} {$description}"); parent::__construct($message); } final public function getStatusCode() { return $this->statusCode; } final public function getURI() { return $this->uri; } abstract public function isError(); abstract public function isTimeout(); abstract protected function getErrorCodeType($code); abstract protected function getErrorCodeDescription($code); } diff --git a/src/future/http/status/HTTPFutureResponseStatusCURL.php b/src/future/http/status/HTTPFutureResponseStatusCURL.php index fe43247..ba77334 100644 --- a/src/future/http/status/HTTPFutureResponseStatusCURL.php +++ b/src/future/http/status/HTTPFutureResponseStatusCURL.php @@ -1,102 +1,86 @@ getStatusCode() == CURLE_OPERATION_TIMEOUTED); } protected function getErrorCodeDescription($code) { $constants = get_defined_constants(); $constant_name = null; foreach ($constants as $constant => $value) { if ($value == $code && preg_match('/^CURLE_/', $constant)) { $constant_name = '<'.$constant.'> '; break; } } $map = array( CURLE_COULDNT_RESOLVE_HOST => 'There was an error resolving the server hostname. Check that you are '. 'connected to the internet and that DNS is correctly configured. (Did '. 'you add the domain to `/etc/hosts` on some other machine, but not '. 'this one?)', CURLE_SSL_CACERT => 'There was an error verifying the SSL Certificate Authority while '. 'negotiating the SSL connection. This usually indicates that you are '. 'using a self-signed certificate but have not added your CA to the '. 'CA bundle. See instructions in "libphutil/resources/ssl/README".', // Apparently there's no error constant for this? In cURL it's // CURLE_SSL_CACERT_BADFILE but there's no corresponding constant in // PHP. 77 => 'The SSL CA Bundle "libphutil/resources/ssl/custom.pem" could not '. 'be read or is not formatted correctly.', CURLE_SSL_CONNECT_ERROR => 'There was an error negotiating the SSL connection. This usually '. 'indicates that the remote host has a bad SSL certificate, or your '. 'local host has some sort of SSL misconfiguration which prevents it '. 'from accepting the CA. If you are using a self-signed certificate, '. 'see instructions in "libphutil/resources/ssl/README".', CURLE_OPERATION_TIMEOUTED => 'The request took too long to complete.', CURLE_SSL_PEER_CERTIFICATE => 'There was an error verifying the SSL connection. This usually '. 'indicates that the remote host has an SSL certificate for a '. 'different domain name than you are connecting with. Make sure the '. 'certificate you have installed is signed for the correct domain.', ); $default_message = "The cURL library raised an error while making a request. You may be ". "able to find more information about this error (error code: {$code}) ". "on the cURL site: http://curl.haxx.se/libcurl/c/libcurl-errors.html"; $detailed_message = idx($map, $code, $default_message); return $constant_name.$detailed_message; } } diff --git a/src/future/http/status/HTTPFutureResponseStatusHTTP.php b/src/future/http/status/HTTPFutureResponseStatusHTTP.php index 329644a..b6de184 100644 --- a/src/future/http/status/HTTPFutureResponseStatusHTTP.php +++ b/src/future/http/status/HTTPFutureResponseStatusHTTP.php @@ -1,61 +1,45 @@ 512) { $excerpt = substr($body, 0, 512).'...'; } else { $excerpt = $body; } $this->excerpt = phutil_utf8ize($excerpt); parent::__construct($status_code); } protected function getErrorCodeType($code) { return 'HTTP'; } public function isError() { return ($this->getStatusCode() < 200) || ($this->getStatusCode() > 299); } public function isTimeout() { return false; } protected function getErrorCodeDescription($code) { static $map = array( 404 => 'Not Found', 500 => 'Internal Server Error', ); return idx($map, $code)."\n".$this->excerpt."\n"; } } diff --git a/src/future/http/status/HTTPFutureResponseStatusParse.php b/src/future/http/status/HTTPFutureResponseStatusParse.php index 64d4558..a1f9ef7 100644 --- a/src/future/http/status/HTTPFutureResponseStatusParse.php +++ b/src/future/http/status/HTTPFutureResponseStatusParse.php @@ -1,50 +1,34 @@ rawResponse = $raw_response; parent::__construct($code); } protected function getErrorCodeType($code) { return 'Parse'; } public function isError() { return true; } public function isTimeout() { return false; } protected function getErrorCodeDescription($code) { return "The remote host returned something other than an HTTP response: ". $this->rawResponse; } } diff --git a/src/future/http/status/HTTPFutureResponseStatusTransport.php b/src/future/http/status/HTTPFutureResponseStatusTransport.php index a4ad002..1e1bd49 100644 --- a/src/future/http/status/HTTPFutureResponseStatusTransport.php +++ b/src/future/http/status/HTTPFutureResponseStatusTransport.php @@ -1,63 +1,47 @@ getStatusCode() == self::ERROR_TIMEOUT); } protected function getErrorCodeDescription($code) { $map = array( self::ERROR_TIMEOUT => 'The request took too long to complete.', self::ERROR_CONNECTION_ABORTED => 'The remote host closed the connection before the request completed.', self::ERROR_CONNECTION_REFUSED => 'The remote host refused the connection. This usually means the '. 'host is not running an HTTP server, or the network is blocking '. 'connections from this machine. Verify you can connect to the '. 'remote host from this host.', self::ERROR_CONNECTION_FAILED => 'Connection could not be initiated. This usually indicates a DNS '. 'problem: verify the domain name is correct, that you can '. 'perform a DNS lookup for it from this machine. (Did you add the '. 'domain to `/etc/hosts` on some other machine, but not this one?) '. 'This might also indicate that you specified the wrong port.', ); return idx($map, $code); } } diff --git a/src/infrastructure/testing/PhutilTestCase.php b/src/infrastructure/testing/PhutilTestCase.php index 7fd4232..f317e14 100644 --- a/src/infrastructure/testing/PhutilTestCase.php +++ b/src/infrastructure/testing/PhutilTestCase.php @@ -1,29 +1,13 @@ language = $language; return $this; } /** * Add translations which will be later used by @{method:translate}. * The parameter is an array of strings (for simple translations) or arrays * (for translastions with variants). The number of items in the array is * language specific. It is `array($singular, $plural)` for English. * * array( * 'color' => 'colour', * '%d beer(s)' => array('%d beer', '%d beers'), * ); * * The arrays can be nested for strings with more variant parts: * * array( * '%d char(s) on %d row(s)' => array( * array('%d char on %d row', '%d char on %d rows'), * array('%d chars on %d row', '%d chars on %d rows'), * ), * ); * * The translation should have the same placeholders as originals. Swapping * parameter order is possible: * * array( * '%s owns %s.' => '%2$s is owned by %1$s.', * ); * * @param array Identifier in key, translation in value. * @return PhutilTranslator Provides fluent interface. */ public function addTranslations(array $translations) { $this->translations = array_merge($this->translations, $translations); return $this; } public function translate($text /* , ... */) { $translation = idx($this->translations, $text, $text); $args = func_get_args(); while (is_array($translation)) { $translation = $this->chooseVariant($translation, next($args)); } array_shift($args); return vsprintf($translation, $args); } private function chooseVariant(array $translations, $variant) { switch ($this->language) { case 'en': list($singular, $plural) = $translations; if ($variant == 1) { return $singular; } return $plural; case 'cs': if ($variant instanceof PhutilPerson) { list($male, $female) = $translations; if ($variant->getSex() == PhutilPerson::SEX_FEMALE) { return $female; } return $male; } list($singular, $paucal, $plural) = $translations; if ($variant == 1) { return $singular; } if ($variant >= 2 && $variant <= 4) { return $paucal; } return $plural; default: throw new Exception("Unknown language '{$this->language}'."); } } /** * Translate date formatted by `$date->format()`. * * @param string Format accepted by `DateTime::format()`. * @param DateTime * @return string Formatted and translated date. */ public function translateDate($format, DateTime $date) { static $format_cache = array(); if (!isset($format_cache[$format])) { $translatable = 'DlSFMaA'; preg_match_all( '/['.$translatable.']|(\\\\.|[^'.$translatable.'])+/', $format, $format_cache[$format], PREG_SET_ORDER); } $parts = array(); foreach ($format_cache[$format] as $match) { $part = $date->format($match[0]); if (!isset($match[1])) { $part = $this->translate($part); } $parts[] = $part; } return implode('', $parts); } public function validateTranslation($original, $translation) { $pattern = '/<(\S[^>]*>?)?|&(\S[^;]*;?)?/i'; $original_matches = null; $translation_matches = null; preg_match_all($pattern, $original, $original_matches); preg_match_all($pattern, $translation, $translation_matches); sort($original_matches[0]); sort($translation_matches[0]); if ($original_matches[0] !== $translation_matches[0]) { return false; } return true; } } diff --git a/src/internationalization/__tests__/PhutilPHTTestCase.php b/src/internationalization/__tests__/PhutilPHTTestCase.php index 21a278c..8a8ffea 100644 --- a/src/internationalization/__tests__/PhutilPHTTestCase.php +++ b/src/internationalization/__tests__/PhutilPHTTestCase.php @@ -1,116 +1,100 @@ assertEqual('beer', pht('beer')); $this->assertEqual('1 beer(s)', pht('%d beer(s)', 1)); PhutilTranslator::getInstance()->addTranslations( array( '%d beer(s)' => array('%d beer', '%d beers'), )); $this->assertEqual('1 beer', pht('%d beer(s)', 1)); PhutilTranslator::getInstance()->setLanguage('cs'); PhutilTranslator::getInstance()->addTranslations( array( '%d beer(s)' => array('%d pivo', '%d piva', '%d piv'), )); $this->assertEqual('5 piv', pht('%d beer(s)', 5)); } public function getDateTranslations() { // The only purpose of this function is to provide a static list of // translations which can come from PhutilTranslator::translateDate() to // allow translation extractor getting them. return array( 'D' => array( pht('Sun'), pht('Mon'), pht('Tue'), pht('Wed'), pht('Thu'), pht('Fri'), pht('Sat'), ), 'l' => array( pht('Sunday'), pht('Monday'), pht('Tuesday'), pht('Wednesday'), pht('Thursday'), pht('Friday'), pht('Saturday'), ), 'S' => array( pht('st'), pht('nd'), pht('rd'), pht('th'), ), 'F' => array( pht('January'), pht('February'), pht('March'), pht('April'), pht('May'), pht('June'), pht('July'), pht('August'), pht('September'), pht('October'), pht('November'), pht('December'), ), 'M' => array( pht('Jan'), pht('Feb'), pht('Mar'), pht('Apr'), pht('May'), pht('Jun'), pht('Jul'), pht('Aug'), pht('Sep'), pht('Oct'), pht('Nov'), pht('Dec'), ), 'a' => array( pht('am'), pht('pm'), ), 'A' => array( pht('AM'), pht('PM'), ), ); } } diff --git a/src/internationalization/__tests__/PhutilPersonTest.php b/src/internationalization/__tests__/PhutilPersonTest.php index 61c7d5d..17ec2f2 100644 --- a/src/internationalization/__tests__/PhutilPersonTest.php +++ b/src/internationalization/__tests__/PhutilPersonTest.php @@ -1,38 +1,22 @@ sex; } public function setSex($value) { $this->sex = $value; return $this; } public function __toString() { return 'Test ('.$this->sex.')'; } } diff --git a/src/internationalization/__tests__/PhutilTranslatorTestCase.php b/src/internationalization/__tests__/PhutilTranslatorTestCase.php index 04bdb74..1b13d2e 100644 --- a/src/internationalization/__tests__/PhutilTranslatorTestCase.php +++ b/src/internationalization/__tests__/PhutilTranslatorTestCase.php @@ -1,165 +1,149 @@ addTranslations( array( '%d line(s)' => array('%d line', '%d lines'), '%d char(s) on %d row(s)' => array( array('%d char on %d row', '%d char on %d rows'), array('%d chars on %d row', '%d chars on %d rows'), ), )); $this->assertEqual('line', $translator->translate('line')); $this->assertEqual('param', $translator->translate('%s', 'param')); $this->assertEqual('0 lines', $translator->translate('%d line(s)', 0)); $this->assertEqual('1 line', $translator->translate('%d line(s)', 1)); $this->assertEqual('2 lines', $translator->translate('%d line(s)', 2)); $this->assertEqual( '1 char on 1 row', $translator->translate('%d char(s) on %d row(s)', 1, 1)); $this->assertEqual( '5 chars on 2 rows', $translator->translate('%d char(s) on %d row(s)', 5, 2)); $this->assertEqual('1 beer(s)', $translator->translate('%d beer(s)', 1)); } public function testCzech() { $translator = new PhutilTranslator(); $translator->setLanguage('cs'); $translator->addTranslations( array( '%d beer(s)' => array('%d pivo', '%d piva', '%d piv'), )); $this->assertEqual('0 piv', $translator->translate('%d beer(s)', 0)); $this->assertEqual('1 pivo', $translator->translate('%d beer(s)', 1)); $this->assertEqual('2 piva', $translator->translate('%d beer(s)', 2)); $this->assertEqual('5 piv', $translator->translate('%d beer(s)', 5)); $this->assertEqual('1 line(s)', $translator->translate('%d line(s)', 1)); } public function testPerson() { $translator = new PhutilTranslator(); $translator->setLanguage('cs'); $translator->addTranslations( array( '%s wrote.' => array('%s napsal.', '%s napsala.'), )); $person = new PhutilPersonTest(); $this->assertEqual( 'Test () napsal.', $translator->translate('%s wrote.', $person)); $person->setSex(PhutilPerson::SEX_MALE); $this->assertEqual( 'Test (m) napsal.', $translator->translate('%s wrote.', $person)); $person->setSex(PhutilPerson::SEX_FEMALE); $this->assertEqual( 'Test (f) napsala.', $translator->translate('%s wrote.', $person)); } public function testTranslateDate() { $date = new DateTime('2012-06-21'); $translator = new PhutilTranslator(); $this->assertEqual('June', $translator->translateDate('F', $date)); $this->assertEqual('June 21', $translator->translateDate('F d', $date)); $this->assertEqual('F', $translator->translateDate('\F', $date)); $translator->addTranslations( array( 'June' => 'correct', '21' => 'wrong', 'F' => 'wrong' )); $this->assertEqual('correct', $translator->translateDate('F', $date)); $this->assertEqual('correct 21', $translator->translateDate('F d', $date)); $this->assertEqual('F', $translator->translateDate('\F', $date)); } public function testSetInstance() { PhutilTranslator::setInstance(new PhutilTranslator()); $original = PhutilTranslator::getInstance(); $this->assertEqual('color', pht('color')); $british = new PhutilTranslator(); $british->addTranslations( array( 'color' => 'colour', )); PhutilTranslator::setInstance($british); $this->assertEqual('colour', pht('color')); PhutilTranslator::setInstance($original); $this->assertEqual('color', pht('color')); } public function testValidateTranslation() { $tests = array( 'a < 2' => array( 'a < 2' => true, 'b < 3' => true, '2 > a' => false, 'a<2' => false, ), 'We win' => array( 'We win' => true, 'We win' => true, // false positive 'We win' => false, 'We win' => false, ), 'We win & triumph' => array( 'We triumph & win' => true, 'We win and triumph' => false, ), 'beer' => array( 'pivo' => true, 'b<>r' => false, 'b&&r' => false, ), ); $translator = new PhutilTranslator(); foreach ($tests as $original => $translations) { foreach ($translations as $translation => $expect) { $valid = ($expect ? "valid" : "invalid"); $this->assertEqual( $expect, $translator->validateTranslation($original, $translation), "'{$original}' should be {$valid} with '{$translation}'."); } } } } diff --git a/src/internationalization/pht.php b/src/internationalization/pht.php index f04f577..1e78972 100644 --- a/src/internationalization/pht.php +++ b/src/internationalization/pht.php @@ -1,36 +1,20 @@ addTranslations()` and language rules set * by `PhutilTranslator::getInstance()->setLanguage()`. * * @param string Translation identifier with sprintf() placeholders. * @param mixed Value to select the variant from (e.g. singular or plural). * @param ... Next values referenced from $text. * @return string Translated string with substituted values. * * @group internationalization */ function pht($text, $variant = null /*, ... */) { $args = func_get_args(); $translator = PhutilTranslator::getInstance(); return call_user_func_array(array($translator, 'translate'), $args); } diff --git a/src/lexer/PhutilLexer.php b/src/lexer/PhutilLexer.php index 3e74df0..cf45b5b 100644 --- a/src/lexer/PhutilLexer.php +++ b/src/lexer/PhutilLexer.php @@ -1,333 +1,317 @@ array(...), * 'state1' => array(...), * 'state2' => array(...), * ) * * Lexers start at the state named 'start'. Each state should have a list of * rules which can match in that state. A list of rules looks like this: * * array( * array('\s+', 'space'), * array('\d+', 'digit'), * array('\w+', 'word'), * ) * * The lexer operates by processing each rule in the current state in order. * When one matches, it produces a token. For example, the lexer above would * lex this text: * * 3 asdf * * ...to produce these tokens (assuming the rules are for the 'start' state): * * array('digit', '3', null), * array('space', ' ', null), * array('word', 'asdf', null), * * A rule can also cause a state transition: * * array('zebra', 'animal', 'saw_zebra'), * * This would match the text "zebra", emit a token of type "animal", and change * the parser state to "saw_zebra", causing the lexer to start using the rules * from that state. * * To pop the lexer's state, you can use the special state '!pop'. * * Finally, you can provide additional options in the fourth parameter. * Supported options are `case-insensitive` and `context`. * * Possible values for `context` are `push` (push the token value onto the * context stack), `pop` (pop the context stack and use it to provide context * for the token), and `discard` (pop the context stack and throw away the * value). * * For example, to lex text like this: * * Class::CONSTANT * * You can use a rule set like this: * * 'start' => array( * array('\w+(?=::)', 'class', 'saw_class', array('context' => 'push')), * ), * 'saw_class' => array( * array('::', 'operator'), * array('\w+', 'constant, '!pop', array('context' => 'pop')), * ), * * This would parse the above text into this token stream: * * array('class', 'Class', null), * array('operator', '::', null), * array('constant', 'CONSTANT', 'Class'), * * For a concrete implementation, see @{class:PhutilPHPFragmentLexer}. * * @task lexerimpl Lexer Implementation * @task rule Lexer Rules * @task tokens Lexer Tokens * * @group lexer */ abstract class PhutilLexer { private $processedRules; /* -( Lexer Rules )-------------------------------------------------------- */ /** * Return a set of rules for this lexer. See description in * @{class:PhutilLexer}. * * @return dict Lexer rules. * @task lexerimpl */ abstract protected function getRawRules(); /* -( Lexer Rules )-------------------------------------------------------- */ /** * Process, normalize, and validate the raw lexer rules. * * @task rule */ protected function getRules() { $class = get_class($this); $raw_rules = $this->getRawRules(); if (!is_array($raw_rules)) { $type = gettype($raw_rules); throw new UnexpectedValueException( "Expected {$class}->getRawRules() to return array, got {$type}."); } if (empty($raw_rules['start'])) { throw new UnexpectedValueException( "Expected {$class} rules to define rules for state 'start'."); } $processed_rules = array(); foreach ($raw_rules as $state => $rules) { if (!is_array($rules)) { $type = gettype($rules); throw new UnexpectedValueException( "Expected list of rules for state '{$state}' in {$class}, got ". "{$type}."); } foreach ($rules as $key => $rule) { $n = count($rule); if ($n < 2 || $n > 4) { throw new UnexpectedValueException( "Expected rule '{$key}' in state '{$state}' in {$class} to have ". "2-4 elements (regex, token, [next state], [options]), got {$n}."); } $rule = array_values($rule); if (count($rule) == 2) { $rule[] = null; } if (count($rule) == 3) { $rule[] = array(); } foreach ($rule[3] as $option => $value) { switch ($option) { case 'context': if ($value !== 'push' && $value !== 'pop' && $value !== 'discard' && $value !== null) { throw new UnexpectedValueException( "Rule '{$key}' in state '{$state}' in {$class} has unknown ". "context rule '{$value}', expected 'push', 'pop' or ". "'discard'."); } break; default: throw new UnexpectedValueException( "Rule '{$key}' in state '{$state}' in {$class} has unknown ". "option '{$option}'."); } } $flags = 'sS'; // NOTE: The "\G" assertion is an offset-aware version of "^". $rule[0] = '(\\G'.$rule[0].')'.$flags; if (@preg_match($rule[0], '') === false) { $error = error_get_last(); throw new UnexpectedValueException( "Rule '{$key}' in state '{$state}' in {$class} defines an ". "invalid regular expression ('{$rule[0]}'): ". idx($error, 'message')); } $next_state = $rule[2]; if ($next_state !== null && $next_state !== '!pop') { if (empty($raw_rules[$next_state])) { throw new UnexpectedValueException( "Rule '{$key}' in state '{$state}' in {$class} transitions to ". "state '{$next_state}', but there are no rules for that state."); } } $processed_rules[$state][] = $rule; } } return $processed_rules; } /* -( Lexer Tokens )------------------------------------------------------- */ /** * Lex an input string into tokens. * * @param string Input string. * @param string Initial lexer state. * @return list List of lexer tokens. * @task tokens */ public function getTokens($input, $initial_state = 'start') { if (empty($this->processedRules)) { $this->processedRules = $this->getRules(); } $rules = $this->processedRules; $position = 0; $length = strlen($input); $tokens = array(); $states = array(); $states[] = 'start'; if ($initial_state != 'start') { $states[] = $initial_state; } $context = array(); while ($position < $length) { $state_rules = idx($rules, end($states), array()); foreach ($state_rules as $rule) { $matches = null; if (!preg_match($rule[0], $input, $matches, 0, $position)) { continue; } list($regexp, $token_type, $next_state, $options) = $rule; $match_length = strlen($matches[0]); if (!$match_length) { if ($next_state === null) { throw new UnexpectedValueException( "Rule '{$regexp}' matched a zero-length token and causes no ". "state transition."); } } else { $position += $match_length; $token = array($token_type, $matches[0]); $copt = idx($options, 'context'); if ($copt == 'push') { $context[] = $matches[0]; $token[] = null; } else if ($copt == 'pop') { if (empty($context)) { throw new UnexpectedValueException( "Rule '{$regexp}' popped empty context!"); } $token[] = array_pop($context); } else if ($copt == 'discard') { if (empty($context)) { throw new UnexpectedValueException( "Rule '{$regexp}' discarded empty context!"); } array_pop($context); $token[] = null; } else { $token[] = null; } $tokens[] = $token; } if ($next_state !== null) { if ($next_state == '!pop') { array_pop($states); if (empty($states)) { throw new UnexpectedValueException( "Rule '{$regexp}' popped off the last state."); } } else { $states[] = $next_state; } } continue 2; } throw new UnexpectedValueException( "No lexer rule matched input at char {$position}."); } return $tokens; } /** * Merge adjacent tokens of the same type. For example, if a comment is * tokenized as <"//", "comment">, this method will merge the two tokens into * a single combined token. */ public function mergeTokens(array $tokens) { $last = null; $result = array(); foreach ($tokens as $token) { if ($last === null) { $last = $token; continue; } if (($token[0] == $last[0]) && ($token[2] == $last[2])) { $last[1] .= $token[1]; } else { $result[] = $last; $last = $token; } } if ($last !== null) { $result[] = $last; } return $result; } } diff --git a/src/lexer/PhutilPHPFragmentLexer.php b/src/lexer/PhutilPHPFragmentLexer.php index 861c41b..22e71bb 100644 --- a/src/lexer/PhutilPHPFragmentLexer.php +++ b/src/lexer/PhutilPHPFragmentLexer.php @@ -1,283 +1,267 @@ array( array('<\\?(?i:php)?', 'cp', 'php'), array('[^<]+', null), array('<', null), ), 'php' => array_merge(array( array('\\?>', 'cp', '!pop'), array( '<<<([\'"]?)('.$identifier_pattern.')\\1\\n.*?\\n\\2\\;?\\n', 's'), ), $nonsemantic_rules, array( array('(?i:__halt_compiler)\\b', 'cp', 'halt_compiler'), array('(->|::)', 'o', 'attr'), array('[~!%^&*+=|:.<>/?@-]+', 'o'), array('[\\[\\]{}();,]', 'o'), // After 'new', try to match an unadorned symbol. array('(?i:new|instanceof)\\b', 'k', 'possible_classname'), array('(?i:function)\\b', 'k', 'function_definition'), // After 'extends' or 'implements', match a list of classes/interfaces. array('(?i:extends|implements)\\b', 'k', 'class_list'), array('(?i:catch)\\b', 'k', 'catch'), array('(?i:'.implode('|', $keywords).')\\b', 'k'), array('(?i:'.implode('|', $constants).')\\b', 'kc'), array('\\$+'.$identifier_pattern, 'nv'), // Match "f(" as a function and "C::" as a class. These won't work // if you put a comment between the symbol and the operator, but // that's a bizarre usage. array($identifier_ns_pattern.'(?=\s*[\\(])', 'nf'), array($identifier_ns_pattern.'(?=\s*::)', 'nc', 'context_attr', array( 'context' => 'push', ), ), array($identifier_ns_pattern, 'no'), array('(\\d+\\.\\d*|\\d*\\.\\d+)([eE][+-]?[0-9]+)?', 'mf'), array('\\d+[eE][+-]?[0-9]+', 'mf'), array('0[0-7]+', 'mo'), array('0[xX][a-fA-F0-9]+', 'mh'), array('0[bB][0-1]+', 'm'), array('\d+', 'mi'), array("'", "s1", 'string1'), array("`", "sb", 'stringb'), array('"', 's2', 'string2'), array('.', null), )), // We've just matched a class name, with a "::" lookahead. The name of // the class is on the top of the context stack. We want to try to match // the attribute or method (e.g., "X::C" or "X::f()"). 'context_attr' => array_merge($nonsemantic_rules, array( array('::', 'o'), array($identifier_pattern.'(?=\s*[\\(])', 'nf', '!pop', array( 'context' => 'pop', ), ), array($identifier_pattern, 'na', '!pop', array( 'context' => 'pop', ), ), array('', null, '!pop', array( 'context' => 'discard', ), ), )), // After '->' or '::', a symbol is an attribute name. Note that we end // up in 'context_attr' instead of here in some cases. 'attr' => array_merge($nonsemantic_rules, array( array($identifier_pattern, 'na', '!pop'), array('', null, '!pop'), )), // After 'new', a symbol is a class name. 'possible_classname' => array_merge($nonsemantic_rules, array( array($identifier_ns_pattern, 'nc', '!pop'), array('', null, '!pop'), )), 'string1' => array( array('[^\'\\\\]+', 's1'), array("'", 's1', '!pop'), array('\\\\.', 'k'), ), 'stringb' => array( array('[^`\\\\]+', 'sb'), array('`', 'sb', '!pop'), array('\\\\.', 'k'), ), 'string2' => array( array('[^"\\\\]+', 's2'), array('"', 's2', '!pop'), array('\\\\.', 'k'), ), // In a function definition (after "function"), we don't link the name // as a "nf" (name.function) since it is its own definition. 'function_definition' => array_merge($nonsemantic_rules, array( array('&', 'o'), array('\\(', 'o', '!pop'), array($identifier_pattern, 'no', '!pop'), array('', null, '!pop'), )), // For "//" and "#" comments, we need to break out if we see "?" followed // by ">". 'line_comment' => array( array('[^?\\n]+', 'c'), array('\\n', null, '!pop'), array('(?=\\?>)', null, '!pop'), array('\\?', 'c'), ), // We've seen __halt_compiler. Grab the '();' afterward and then eat // the rest of the file as raw data. 'halt_compiler' => array_merge($nonsemantic_rules, array( array('[()]', 'o'), array(';', 'o', 'compiler_halted'), array('\\?>', 'o', 'compiler_halted'), // Just halt on anything else. array('', null, 'compiler_halted'), )), // __halt_compiler has taken effect. 'compiler_halted' => array( array('.+', null), ), 'class_list' => array_merge($nonsemantic_rules, array( array(',', 'o'), array('(?i:implements)', 'k'), array($identifier_ns_pattern, 'nc'), array('', null, '!pop'), )), 'catch' => array_merge($nonsemantic_rules, array( array('\\(', 'o'), array($identifier_ns_pattern, 'nc'), array('', null, '!pop'), )), ); } } diff --git a/src/lexer/__tests__/PhutilPHPFragmentLexerTestCase.php b/src/lexer/__tests__/PhutilPHPFragmentLexerTestCase.php index 19c9d94..caacf05 100644 --- a/src/lexer/__tests__/PhutilPHPFragmentLexerTestCase.php +++ b/src/lexer/__tests__/PhutilPHPFragmentLexerTestCase.php @@ -1,324 +1,308 @@ runLexer($file, $data); } } private function runLexer($file, $data) { $lexer = new PhutilPHPFragmentLexer(); switch ($file) { case 'pop-from-php.txt': $initial_state = 'php'; break; default: $initial_state = 'start'; break; } $caught = null; $tokens = null; try { $tokens = $lexer->getTokens($data, $initial_state); } catch (Exception $ex) { $caught = $ex; } switch ($file) { case 'basics.txt': $this->assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $tokens = $lexer->mergeTokens($tokens); $this->assertEqual( array( array('cp', '', null), array(null, "\n\nd\n", null), ), $tokens, $file); break; case 'extendsimplements.txt': $this->assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', 'assertEqual(null, $caught); $this->assertEqual( array( array('cp', '?>', null), array(null, "\n", null), ), $tokens, $file); break; default: throw new Exception("No assertion block for test '{$file}'!"); } } } diff --git a/src/markup/PhutilMarkupEngine.php b/src/markup/PhutilMarkupEngine.php index ed8567e..865e74e 100644 --- a/src/markup/PhutilMarkupEngine.php +++ b/src/markup/PhutilMarkupEngine.php @@ -1,51 +1,35 @@ assertEqual( phutil_render_tag('x'), phutil_render_tag('x', array())); $this->assertEqual( phutil_render_tag('x', array()), phutil_render_tag('x', array(), null)); } public function testTagEmpty() { $this->assertEqual( '', phutil_render_tag('x', array(), null)); $this->assertEqual( '', phutil_render_tag('x', array(), '')); } public function testTagBasics() { $this->assertEqual( '', phutil_render_tag('x')); $this->assertEqual( 'y', phutil_render_tag('x', array(), 'y')); } public function testTagAttributes() { $this->assertEqual( 'y', phutil_render_tag('x', array('u' => 'v'), 'y')); $this->assertEqual( '', phutil_render_tag('x', array('u' => 'v'))); } public function testTagEscapes() { $this->assertEqual( '', phutil_render_tag('x', array('u' => '<'))); $this->assertEqual( '', phutil_render_tag('x', array(), phutil_render_tag('y'))); } public function testTagNullAttribute() { $this->assertEqual( '', phutil_render_tag('x', array('y' => null))); } public function testTagJavascriptProtocolRejection() { $hrefs = array( 'javascript:alert(1)' => true, 'JAVASCRIPT:alert(1)' => true, ' javascript:alert(1)' => true, '/' => false, '/path/to/stuff/' => false, '' => false, 'http://example.com/' => false, '#' => false, ); foreach (array(true, false) as $use_uri) { foreach ($hrefs as $href => $expect) { if ($use_uri) { $href = new PhutilURI($href); } $caught = null; try { phutil_render_tag('a', array('href' => $href), 'click for candy'); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( $expect, $caught instanceof Exception, "Rejected href: {$href}"); } } } public function testURIEscape() { $this->assertEqual( '%2B/%20%3F%23%26%3A%21xyz%25', phutil_escape_uri('+/ ?#&:!xyz%')); } public function testURIPathComponentEscape() { $this->assertEqual( 'a%252Fb', phutil_escape_uri_path_component('a/b')); $str = ''; for ($ii = 0; $ii <= 255; $ii++) { $str .= chr($ii); } $this->assertEqual( $str, phutil_unescape_uri_path_component( rawurldecode( // Simulates webserver. phutil_escape_uri_path_component($str)))); } public function testHsprintf() { $this->assertEqual( '
<3
', hsprintf('
%s
', '<3')); } } diff --git a/src/markup/engine/PhutilRemarkupEngine.php b/src/markup/engine/PhutilRemarkupEngine.php index 301c829..4210ab4 100644 --- a/src/markup/engine/PhutilRemarkupEngine.php +++ b/src/markup/engine/PhutilRemarkupEngine.php @@ -1,166 +1,150 @@ config[$key] = $value; return $this; } public function getConfig($key, $default = null) { return idx($this->config, $key, $default); } public function setBlockRules(array $rules) { assert_instances_of($rules, 'PhutilRemarkupEngineBlockRule'); $this->blockRules = $rules; return $this; } public function getTextMetadata($key, $default = null) { return idx($this->metadata, $key, $default); } public function setTextMetadata($key, $value) { $this->metadata[$key] = $value; return $this; } public function storeText($text) { return $this->storage->store($text); } public function overwriteStoredText($token, $new_text) { $this->storage->overwrite($token, $new_text); return $this; } public function markupText($text) { return $this->postprocessText($this->preprocessText($text)); } private function setupProcessing() { $this->metadata = array(); $this->storage = new PhutilRemarkupBlockStorage(); $block_rules = $this->blockRules; if (empty($block_rules)) { throw new Exception("Remarkup engine not configured with block rules."); } foreach ($block_rules as $rule) { $rule->setEngine($this); } } public function preprocessText($text) { $this->setupProcessing(); // Apply basic block and paragraph normalization to the text. NOTE: We don't // strip trailing whitespace because it is semantic in some contexts, // notably inlined diffs that the author intends to show as a code block. $text = preg_replace("/\r\n?/", "\n", $text); $text = preg_split("/\n\n/", $text); $block_rules = $this->blockRules; $blocks = array(); $last = null; $last_block = null; foreach ($text as $block) { $action = null; if ($last !== null) { if ($block_rules[$last]->shouldContinueWithBlock($block, $last_block)) { $action = 'merge'; } } if (!$action) { foreach ($block_rules as $key => $block_rule) { if (!$block_rule->shouldMatchBlock(trim($block, "\n"))) { continue; } if (($last !== null) && ($key == $last) && $block_rule->shouldMergeBlocks()) { $action = 'merge'; } else { $action = 'append'; } $last = $key; break; } } $last_block = $block; switch ($action) { case 'merge': end($blocks); $last_block_key = key($blocks); $blocks[$last_block_key]['block'] .= "\n\n".$block; $last_block = $blocks[$last_block_key]['block']; break; case 'append': $blocks[] = array( 'rule' => $block_rules[$last], 'block' => $block, ); break; default: throw new Exception("Block in text did not match any block rule."); } } $output = array(); foreach ($blocks as $block) { $output[] = $block['rule']->markupText($block['block']); } $map = $this->storage->getMap(); unset($this->storage); $metadata = $this->metadata; return array( 'output' => implode("\n\n", $output), 'storage' => $map, 'metadata' => $metadata, ); } public function postprocessText(array $dict) { $this->setupProcessing(); $this->metadata = idx($dict, 'metadata', array()); $this->storage->setMap(idx($dict, 'storage', array())); foreach ($this->blockRules as $block_rule) { $block_rule->postprocess(); } return $this->storage->restore(idx($dict, 'output')); } } diff --git a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php index f5cee11..4cae6ac 100644 --- a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php +++ b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php @@ -1,103 +1,87 @@ markupText($root.$file); } } private function markupText($markup_file) { $contents = Filesystem::readFile($markup_file); $file = basename($markup_file); $parts = explode("\n~~~~~~~~~~\n", $contents); $this->assertEqual(2, count($parts)); list($input_remarkup, $expected_output) = $parts; switch ($file) { case 'raw-escape.txt': // NOTE: Here, we want to test PhutilRemarkupRuleEscapeRemarkup and // PhutilRemarkupBlockStorage, which are triggered by "\1". In the // test, "~" is used as a placeholder for "\1" since it's hard to type // "\1". $input_remarkup = str_replace("~", "\1", $input_remarkup); $expected_output = str_replace("~", "\1", $expected_output); break; } $engine = $this->buildNewTestEngine(); $actual_output = $engine->markupText($input_remarkup); $this->assertEqual( $expected_output, $actual_output, "Failed to markup file '{$file}'."); } private function buildNewTestEngine() { $engine = new PhutilRemarkupEngine(); $engine->setConfig( 'uri.allowed-protocols', array( 'http' => true, )); $rules = array(); $rules[] = new PhutilRemarkupRuleEscapeRemarkup(); $rules[] = new PhutilRemarkupRuleMonospace(); $rules[] = new PhutilRemarkupRuleDocumentLink(); $rules[] = new PhutilRemarkupRuleHyperlink(); $rules[] = new PhutilRemarkupRuleEscapeHTML(); $rules[] = new PhutilRemarkupRuleBold(); $rules[] = new PhutilRemarkupRuleItalic(); $rules[] = new PhutilRemarkupRuleDel(); $blocks = array(); $blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupSimpleTableBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule(); foreach ($blocks as $block) { if (!($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) { $block->setMarkupRules($rules); } } $engine->setBlockRules($blocks); return $engine; } } diff --git a/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php b/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php index ef44eb9..7284812 100644 --- a/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php +++ b/src/markup/engine/remarkup/PhutilRemarkupBlockStorage.php @@ -1,87 +1,71 @@ \11Z
* * Then: * * ... * * If we didn't do this, the italics rule could match the "//" in "http://", * or any other number of processing mistakes could occur, some of which create * security risks. * * This class generates keys, and stores the map of keys to replacement text. * * @group markup */ final class PhutilRemarkupBlockStorage { private $map = array(); private $index; public function store($text) { $key = "\1".(++$this->index)."Z"; $this->map[$key] = $text; return $key; } public function restore($corpus) { if ($this->map) { $corpus = str_replace( array_reverse(array_keys($this->map)), array_reverse($this->map), $corpus); $this->map = array(); } return $corpus; } public function overwrite($key, $new_text) { $this->map[$key] = $new_text; return $this; } public function getMap() { return $this->map; } public function setMap(array $map) { $this->map = $map; return $this; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php index 82081bb..76ba6a0 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php @@ -1,82 +1,66 @@ getBlockPattern(), $block); } public function shouldContinueWithBlock($block, $last_block) { return false; } final public function setEngine(PhutilRemarkupEngine $engine) { $this->engine = $engine; return $this; } final protected function getEngine() { return $this->engine; } public function setMarkupRules(array $rules) { assert_instances_of($rules, 'PhutilRemarkupRule'); $this->rules = $rules; return $this; } final private function getMarkupRules() { return $this->rules; } final public function postprocess() { $engine = $this->getEngine(); $this->didMarkupText(); foreach ($this->getMarkupRules() as $rule) { $rule->setEngine($engine); $rule->didMarkupText(); } } final protected function applyRules($text) { $engine = $this->getEngine(); foreach ($this->getMarkupRules() as $rule) { $rule->setEngine($engine); $text = $rule->apply($text); } return $text; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php index 85bc460..d9c24cb 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php @@ -1,179 +1,163 @@ getBlockPattern(), $block)) { return false; } if (preg_match('@^[a-z]+://\S+$@', trim($block))) { return false; } return true; } public function shouldContinueWithBlock($block, $last_block) { // If the first code block begins with ```, we keep matching blocks until // we hit a terminating ```, regardless of their content. if (preg_match('/^```/', $last_block)) { if (preg_match('/```$/', $last_block)) { return false; } return true; } // If we just matched a code block based on indentation, always match the // next block if it is indented, too. This basically means that we'll treat // lists after code blocks as more code, but usually the "-" is from a diff // or from objective C or something; it is rare to intentionally follow a // code block with a list. if (preg_match('/^\s{2,}/', $block)) { return true; } return false; } public function shouldMergeBlocks() { return true; } public function markupText($text) { if (preg_match('/^```/', $text)) { // If this is a ```-style block, trim off the backticks. $text = preg_replace('/```\s*$/', '', substr($text, 3)); } $lines = explode("\n", $text); $options = array( 'counterexample' => false, 'lang' => null, 'name' => null, 'lines' => null, ); $custom = PhutilSimpleOptions::parse(head($lines)); if ($custom) { $valid = true; foreach ($custom as $key => $value) { if (!array_key_exists($key, $options)) { $valid = false; break; } } if ($valid) { array_shift($lines); $options = $custom + $options; } } if ($options['counterexample']) { $aux_class = ' remarkup-counterexample'; } else { $aux_class = null; } // Normalize the text back to a 0-level indent. $min_indent = 80; foreach ($lines as $line) { for ($ii = 0; $ii < strlen($line); $ii++) { if ($line[$ii] != ' ') { $min_indent = min($ii, $min_indent); break; } } } if ($min_indent) { $indent_string = str_repeat(' ', $min_indent); $text = preg_replace( '/^'.$indent_string.'/m', '', implode("\n", $lines)); } else { $text = implode("\n", $lines); } if (empty($options['lang'])) { // If the user hasn't specified "lang=..." explicitly, try to guess the // language. If we fail, fall back to configured defaults. $lang = PhutilLanguageGuesser::guessLanguage($text); if (!$lang) { $lang = nonempty( $this->getEngine()->getConfig('phutil.codeblock.language-default'), 'php'); } $options['lang'] = $lang; } $name_header = null; if ($options['name']) { $name_header = phutil_render_tag( 'div', array( 'class' => 'remarkup-code-header', ), phutil_escape_html($options['name'])); } $aux_style = null; if ($options['lines']) { // Put a minimum size on this because the scrollbar is otherwise // unusable. $height = max(6, (int)$options['lines']); $aux_style = 'max-height: '.(2 * $height).'em;'; } $engine = $this->getEngine()->getConfig('syntax-highlighter.engine'); if (!$engine) { $engine = 'PhutilDefaultSyntaxHighlighterEngine'; } $engine = newv($engine, array()); $engine->setConfig( 'pygments.enabled', $this->getEngine()->getConfig('pygments.enabled')); $code_body = phutil_render_tag( 'pre', array( 'class' => 'remarkup-code'.$aux_class, 'style' => $aux_style, ), $engine->highlightSource($options['lang'], $text)); return phutil_render_tag( 'div', array( 'class' => 'remarkup-code-block', 'data-code-lang' => $options['lang'], ), $name_header.$code_body); } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php index 1a73e4b..09a93bd 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php @@ -1,45 +1,29 @@ applyRules($text); $lines = explode("\n", trim($text)); $implode_on = $this->getEngine()->getConfig('preserve-linebreaks') ? '
' : ''; return '

'.trim(implode($implode_on."\n", $lines)).'

'; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php index decc8d4..ec8ba1e 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php @@ -1,133 +1,117 @@ getEngine(); $use_anchors = $engine->getConfig('header.generate-toc'); $anchor = null; if ($use_anchors) { $anchor = $this->generateAnchor($level - 1, $text); } return ''.$anchor.$this->applyRules($text).''; } private function generateAnchor($level, $text) { $anchor = strtolower($text); $anchor = preg_replace('/[^a-z0-9]/', '-', $anchor); $anchor = preg_replace('/--+/', '-', $anchor); $anchor = trim($anchor, '-'); $anchor = substr($anchor, 0, 24); $anchor = trim($anchor, '-'); $base = $anchor; $key = self::KEY_HEADER_TOC; $engine = $this->getEngine(); $anchors = $engine->getTextMetadata($key, array()); $suffix = 1; while (!strlen($anchor) || isset($anchors[$anchor])) { $anchor = $base.'-'.$suffix; $anchor = trim($anchor, '-'); $suffix++; } $anchors[$anchor] = array($level, $text); $engine->setTextMetadata($key, $anchors); return phutil_render_tag( 'a', array( 'name' => $anchor, ), ''); } public static function renderTableOfContents(PhutilRemarkupEngine $engine) { $key = self::KEY_HEADER_TOC; $anchors = $engine->getTextMetadata($key, array()); if (count($anchors) < 2) { // Don't generate a TOC if there are no headers, or if there's only // one header (since such a TOC would be silly). return null; } $depth = 0; $toc = array(); foreach ($anchors as $anchor => $info) { list($level, $name) = $info; while ($depth < $level) { $toc[] = '
    '; $depth++; } while ($depth > $level) { $toc[] = '
'; $depth--; } $toc[] = phutil_render_tag( 'li', array(), phutil_render_tag( 'a', array( 'href' => '#'.$anchor, ), phutil_escape_html($name))); } while ($depth > 0) { $toc[] = ''; $depth--; } return implode("\n", $toc); } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php index 4c8fc69..135326a 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php @@ -1,37 +1,21 @@ applyRules($text); } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php index dd6c9c7..299a335 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php @@ -1,370 +1,354 @@ getBlockPattern(), $line)) { if (preg_match('/^(\s+)/', $line, $matches)) { $space = strlen($matches[1]); } else { $space = 0; } $min_space = min($min_space, $space); } } if ($min_space) { foreach ($lines as $key => $line) { if (preg_match($this->getBlockPattern(), $line)) { $lines[$key] = substr($line, $min_space); } } } // The input text may have linewraps in it, like this: // // - derp derp derp derp // derp derp derp derp // - blarp blarp blarp blarp // // Group text lines together into list items, stored in $items. So the // result in the above case will be: // // array( // array( // "- derp derp derp derp", // " derp derp derp derp", // ), // array( // "- blarp blarp blarp blarp", // ), // ); $item = array(); foreach ($lines as $line) { if (preg_match($this->getBlockPattern(), $line)) { if ($item) { $items[] = $item; $item = array(); } } $item[] = $line; } if ($item) { $items[] = $item; } // Process each item to normalize the text, remove line wrapping, and // determine its depth (indentation level) and style (ordered vs unordered). // // Given the above example, the processed array will look like: // // array( // array( // 'text' => 'derp derp derp derp derp derp derp derp', // 'depth' => 0, // 'style' => '-', // ), // array( // 'text' => 'blarp blarp blarp blarp', // 'depth' => 0, // 'style' => '-', // ), // ); foreach ($items as $key => $item) { $item = preg_replace('/\s*\n\s*/', ' ', implode("\n", $item)); $item = rtrim($item); if (!strlen($item)) { unset($items[$key]); continue; } $matches = null; if (preg_match('/^\s*([-*#]{2,})/', $item, $matches)) { // Alternate-style indents; use number of list item symbols. $depth = strlen($matches[1]) - 1; } else if (preg_match('/^(\s+)/', $item, $matches)) { // Markdown-style indents; use indent depth. $depth = strlen($matches[1]); } else { $depth = 0; } if (preg_match('/^\s*#/', $item)) { $style = '#'; } else { $style = '-'; } $items[$key] = array( 'text' => preg_replace($this->getBlockPattern(), '', $item), 'depth' => $depth, 'style' => $style, ); } $items = array_values($items); // Users can create a sub-list by indenting any deeper amount than the // previous list, so these are both valid: // // - a // - b // // - a // - b // // In the former case, we'll have depths (0, 2). In the latter case, depths // (0, 4). We don't actually care about how many spaces there are, only // how many list indentation levels (that is, we want to map both of // those cases to (0, 1), indicating "outermost list" and "first sublist"). // // This is made more complicated because lists at two different indentation // levels might be at the same list level: // // - a // - b // - c // - d // // Here, 'b' and 'd' are at the same list level (2) but different indent // levels (2, 4). // // Users can also create "staircases" like this: // // - a // - b // # c // // While this is silly, we'd like to render it as faithfully as possible. // // In order to do this, we convert the list of nodes into a tree, // normalizing indentation levels and inserting dummy nodes as necessary to // make the tree well-formed. See additional notes at buildTree(). // // In the case above, the result is a tree like this: // // - // - // - a // - b // # c $l = 0; $r = count($items); $tree = $this->buildTree($items, $l, $r, $cur_level = 0); // We may need to open a list on a node, but they do not have // list style information yet. We need to propagate list style inforamtion // backward through the tree. In the above example, the tree now looks // like this: // // - // - // - a // - b // # c $this->adjustTreeStyleInformation($tree); // Finally, we have enough information to render the tree. $out = $this->renderTree($tree); return implode('', $out); } /** * See additional notes in markupText(). */ private function buildTree(array $items, $l, $r, $cur_level) { if ($l == $r) { return array(); } if ($cur_level > self::MAXIMUM_LIST_NESTING_DEPTH) { // This algorithm is recursive and we don't need you blowing the stack // with your oh-so-clever 50,000-item-deep list. Cap indentation levels // at a reasonable number and just shove everything deeper up to this // level. $nodes = array(); for ($ii = $l; $ii < $r; $ii++) { $nodes[] = array( 'level' => $cur_level, 'items' => array(), ) + $items[$ii]; } return $nodes; } $min = $l; for ($ii = $r - 1; $ii >= $l; $ii--) { if ($items[$ii]['depth'] < $items[$min]['depth']) { $min = $ii; } } $min_depth = $items[$min]['depth']; $nodes = array(); if ($min != $l) { $nodes[] = array( 'text' => null, 'level' => $cur_level, 'style' => null, 'items' => $this->buildTree($items, $l, $min, $cur_level + 1), ); } $last = $min; for ($ii = $last + 1; $ii < $r; $ii++) { if ($items[$ii]['depth'] == $min_depth) { $nodes[] = array( 'level' => $cur_level, 'items' => $this->buildTree($items, $last + 1, $ii, $cur_level + 1), ) + $items[$last]; $last = $ii; } } $nodes[] = array( 'level' => $cur_level, 'items' => $this->buildTree($items, $last + 1, $r, $cur_level + 1), ) + $items[$last]; return $nodes; } /** * See additional notes in markupText(). */ private function adjustTreeStyleInformation(array &$tree) { // The effect here is just to walk backward through the nodes at this level // and apply the first style in the list to any empty nodes we inserted // before it. As we go, also recurse down the tree. $style = '-'; for ($ii = count($tree) - 1; $ii >= 0; $ii--) { if ($tree[$ii]['style'] !== null) { // This is the earliest node we've seen with style, so set the // style to its style. $style = $tree[$ii]['style']; } else { // This node has no style, so apply the current style. $tree[$ii]['style'] = $style; } if ($tree[$ii]['items']) { $this->adjustTreeStyleInformation($tree[$ii]['items']); } } } /** * See additional notes in markupText(). */ private function renderTree(array $tree) { $style = idx(head($tree), 'style'); $out = array(); switch ($style) { case '#': $out[] = "
    \n"; break; case '-': $out[] = "
      \n"; break; } foreach ($tree as $item) { if ($item['text'] === null) { $out[] = '
    • '; } else { $out[] = '
    • '; $out[] = $this->applyRules($item['text']); } if ($item['items']) { foreach ($this->renderTree($item['items']) as $i) { $out[] = $i; } } $out[] = "
    • \n"; } switch ($style) { case '#': $out[] = '
'; break; case '-': $out[] = ''; break; } return $out; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php index 555c0b0..419f1b4 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php @@ -1,51 +1,35 @@ getBlockPattern(), $last_block)) { if (preg_match('/%%%$/', $last_block)) { return false; } return true; } return false; } public function shouldMergeBlocks() { return true; } public function markupText($text) { $text = preg_replace('/%%%\s*$/', '', substr($text, 3)); $text = $this->applyRules($text); return $text; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php index 182a7fe..975a517 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php @@ -1,39 +1,23 @@ '. $this->applyRules($text). ''; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php index 3940a00..497a4f4 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php @@ -1,40 +1,24 @@ /"; } public function shouldMergeBlocks() { return false; } public function markupText($text) { $lines = array(); foreach (explode("\n", $text) as $line) { $lines[] = $this->applyRules(preg_replace('/^>/', '', $line)); } return '
'.implode('
', $lines).'
'; } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php index 3a6012f..6451dfe 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php @@ -1,86 +1,70 @@ 'td', 'content' => $cell); } if (!$headings) { $rows[] = $cells; } else if ($rows) { // Mark previous row with headings. foreach ($cells as $i => $cell) { if ($cell['content']) { $rows[last_key($rows)][$i]['type'] = 'th'; } } } } if (!$rows) { return $this->applyRules($text); } $out = array(); $out[] = "\n"; foreach ($rows as $cells) { $out[] = ''; foreach ($cells as $cell) { $out[] = '<'.$cell['type'].'>'; $out[] = $this->applyRules($cell['content']); $out[] = ''; } $out[] = "\n"; } $out[] = "
\n"; return implode($out); } } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php index c096d83..5fe6ecf 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php @@ -1,131 +1,115 @@ /i'; } public function shouldContinueWithBlock($block, $last_block) { // Until we consume a '', keep merging blocks. if (preg_match('@$@i', $last_block)) { return false; } return true; } public function shouldMergeBlocks() { return true; } public function markupText($text) { $matches = array(); if (!preg_match('@^(.*)
$@si', $text, $matches)) { return $this->fail( $text, 'Bad table (expected ...
)'); } $body = $matches[1]; $row_fragment = '(?:\s*(.*)\s*)'; $cell_fragment = '(?:\s*<(td|th)>(.*)\s*)'; // Test that the body contains only valid rows. if (!preg_match('@^'.$row_fragment.'+$@Usi', $body)) { return $this->fail( $body, 'Bad table syntax (expected rows ...)'); } // Capture the rows. $row_regex = '@'.$row_fragment.'@Usi'; if (!preg_match_all($row_regex, $body, $matches, PREG_SET_ORDER)) { throw new Exception( "Bug in Remarkup tables, parsing fails for input: ".$text); } $out_rows = array(); $rows = $matches; foreach ($rows as $row) { $content = $row[1]; // Test that the row contains only valid cells. if (!preg_match('@^'.$cell_fragment.'+$@Usi', $content)) { return $this->fail( $content, 'Bad table syntax (expected cells ...)'); } // Capture the cells. $cell_regex = '@'.$cell_fragment.'@Usi'; if (!preg_match_all($cell_regex, $content, $matches, PREG_SET_ORDER)) { throw new Exception( "Bug in Remarkup tables, parsing fails for input: ".$text); } $out_cells = array(); foreach ($matches as $cell) { $cell_type = $cell[1]; $cell_content = $cell[2]; $out_cells[] = array( 'type' => $cell_type, 'content' => $cell_content, ); } $out_rows[] = array( 'type' => 'tr', 'content' => $out_cells, ); } $out = array(); $out[] = ''; foreach ($out_rows as $row) { $out[] = '<'.$row['type'].'>'; foreach ($row['content'] as $cell) { $out[] = '<'.$cell['type'].'>'; $out[] = $this->applyRules($cell['content']); $out[] = ''; } $out[] = ''; } $out[] = '
'; return implode("\n", $out); } private function fail($near, $message) { return '
'. phutil_escape_html($message).' near: '. phutil_escape_html(phutil_utf8_shorten($near, 32000)). '
'; } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php index f158b67..9a1da5e 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRule.php @@ -1,41 +1,25 @@ engine = $engine; return $this; } public function getEngine() { return $this->engine; } abstract public function apply($text); public function didMarkupText() { return; } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php index d914424..cd4c032 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php @@ -1,32 +1,16 @@ \1', $text); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php index 0573694..21ad2aa 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php @@ -1,32 +1,16 @@ \1', $text); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php index 76d4651..86de163 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php @@ -1,71 +1,55 @@ $link, 'target' => '_blank', ), phutil_escape_html($name)); } public function markupDocumentLink($matches) { $uri = trim($matches[1]); $name = trim(idx($matches, 2, $uri)); // If whatever is being linked to begins with "/" or has "://", treat it // as a URI instead of a wiki page. $is_uri = preg_match('@(^/)|(://)@', $uri); if ($is_uri && strncmp('/', $uri, 1)) { $protocols = $this->getEngine()->getConfig( 'uri.allowed-protocols', array()); $protocol = id(new PhutilURI($uri))->getProtocol(); if (!idx($protocols, $protocol)) { // Don't treat this as a URI if it's not an allowed protocol. $is_uri = false; } } if (!$is_uri) { return $matches[0]; } return $this->getEngine()->storeText($this->renderHyperlink($uri, $name)); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php index 5045884..7f4d42c 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php @@ -1,29 +1,13 @@ getEngine()->storeText("\1"); return str_replace("\1", $replace, $text); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php index 6b5320d..3349cd1 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php @@ -1,114 +1,98 @@ " around them get linked exactly, without // the "<>". Angle brackets are basically special and mean "this is a URL // with weird characters". This is assumed to be reasonable because they // don't appear in normal text or normal URLs. $text = preg_replace_callback( '@[<](\w{3,}://.+?)[>]@', array($this, 'markupHyperlink'), $text); // Hyperlinks with explicit "()" around them get linked exactly, with the // "()". This is simlar to angle brackets, except that people use parens // in normal text so we preserve them in the output text. $text = preg_replace_callback( '@(?<=\\()(\w{3,}://.+?)(?=\\))@', array($this, 'markupHyperlink'), $text); // Anything else we match "ungreedily", which means we'll look for // stuff that's probably puncutation or otherwise not part of the URL and // not link it. This lets someone write "QuicK! Go to // http://www.example.com/!". We also apply some paren balancing rules. $text = preg_replace_callback( '@(?<=^|\s)(\w{3,}://\S+)(?=\s|$)@', array($this, 'markupHyperlinkUngreedy'), $text); return $text; } protected function markupHyperlink($matches) { $protocols = $this->getEngine()->getConfig( 'uri.allowed-protocols', array()); $protocol = id(new PhutilURI($matches[1]))->getProtocol(); if (!idx($protocols, $protocol)) { // If this URI doesn't use a whitelisted protocol, don't link it. This // is primarily intended to prevent javascript:// silliness. return $this->getEngine()->storeText(phutil_escape_html($matches[1])); } return $this->storeRenderedHyperlink($matches[1]); } protected function storeRenderedHyperlink($link) { return $this->getEngine()->storeText($this->renderHyperlink($link)); } protected function renderHyperlink($link) { return phutil_render_tag( 'a', array( 'href' => $link, 'target' => '_blank', ), phutil_escape_html($link)); } private function markupHyperlinkUngreedy($matches) { $match = $matches[1]; $tail = null; $trailing = null; if (preg_match('/[;,.:!?]+$/', $match, $trailing)) { $tail = $trailing[0]; $match = substr($match, 0, -strlen($tail)); } // If there's a closing paren at the end but no balancing open paren in // the URL, don't link the close paren. This is an attempt to gracefully // handle the two common paren cases, Wikipedia links and English language // parentheticals, e.g.: // // http://en.wikipedia.org/wiki/Noun_(disambiguation) // (see also http://www.example.com) // // We could apply a craftier heuristic here which tries to actually balance // the parens, but this is probably sufficient. if (preg_match('/\\)$/', $match) && !preg_match('/\\(/', $match)) { $tail = ')'.$tail; $match = substr($match, 0, -1); } return $this->markupHyperlink(array(null, $match)).$tail; } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php index d5b3958..871dfa5 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php @@ -1,32 +1,16 @@ \1', $text); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php index 16b5508..35c48a3 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php @@ -1,32 +1,16 @@ \n", $text); } } diff --git a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php index 344e395..4ad9697 100644 --- a/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php +++ b/src/markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php @@ -1,40 +1,24 @@ '.phutil_escape_html($match).''; return $this->getEngine()->storeText($result); } } diff --git a/src/markup/render.php b/src/markup/render.php index 472cde9..76d61ad 100644 --- a/src/markup/render.php +++ b/src/markup/render.php @@ -1,152 +1,136 @@ $v) { if ($v === null) { continue; } $v = phutil_escape_html($v); $attributes[$k] = ' '.$k.'="'.$v.'"'; } $attributes = implode('', $attributes); if ($content === null) { return '<'.$tag.$attributes.' />'; } else { return '<'.$tag.$attributes.'>'.$content.''; } } /** * @group markup */ function phutil_escape_html($string) { return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); } /** * Format a HTML code. This function behaves like sprintf(), except that all * the normal conversions (like %s) will be properly escaped. * * @group markup */ function hsprintf($html/*, ... */) { $args = func_get_args(); array_shift($args); return vsprintf($html, array_map('phutil_escape_html', $args)); } /** * Escape text for inclusion in a URI or a query parameter. Note that this * method does NOT escape '/', because "%2F" is invalid in paths and Apache * will automatically 404 the page if it's present. This will produce correct * (the URIs will work) and desirable (the URIs will be readable) behavior in * these cases: * * '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter * '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix * * It will potentially produce the WRONG behavior in this special case: * * COUNTEREXAMPLE * '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix * * In this case, any '/' characters in the string will not be escaped, so you * will not be able to distinguish between the string and the suffix (unless * you have more information, like you know the format of the suffix). For infix * URI components, use @{function:phutil_escape_uri_path_component} instead. * * @param string Some string. * @return string URI encoded string, except for '/'. * * @group markup */ function phutil_escape_uri($string) { return str_replace('%2F', '/', rawurlencode($string)); } /** * Escape text for inclusion as an infix URI substring. See discussion at * @{function:phutil_escape_uri}. This function covers an unusual special case; * @{function:phutil_escape_uri} is usually the correct function to use. * * This function will escape a string into a format which is safe to put into * a URI path and which does not contain '/' so it can be correctly parsed when * embedded as a URI infix component. * * However, you MUST decode the string with * @{function:phutil_unescape_uri_path_component} before it can be used in the * application. * * @param string Some string. * @return string URI encoded string that is safe for infix composition. * * @group markup */ function phutil_escape_uri_path_component($string) { return rawurlencode(rawurlencode($string)); } /** * Unescape text that was escaped by * @{function:phutil_escape_uri_path_component}. See * @{function:phutil_escape_uri} for discussion. * * Note that this function is NOT the inverse of * @{function:phutil_escape_uri_path_component}! It undoes additional escaping * which is added to survive the implied unescaping performed by the webserver * when interpreting the request. * * @param string Some string emitted * from @{function:phutil_escape_uri_path_component} and * then accessed via a web server. * @return string Original string. * @group markup */ function phutil_unescape_uri_path_component($string) { return rawurldecode($string); } diff --git a/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php b/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php index 6812fae..48c148a 100644 --- a/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php +++ b/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php @@ -1,106 +1,90 @@ config[$key] = $value; return $this; } public function getLanguageFromFilename($filename) { static $default_map = array( // All files which have file extensions that we haven't already matched // map to their extensions. '@\\.([^./]+)$@' => 1, ); $maps = array(); if (!empty($this->config['filename.map'])) { $maps[] = $this->config['filename.map']; } $maps[] = $default_map; foreach ($maps as $map) { foreach ($map as $regexp => $lang) { $matches = null; if (preg_match($regexp, $filename, $matches)) { if (is_numeric($lang)) { return idx($matches, $lang); } else { return $lang; } } } } return null; } public function getHighlightFuture($language, $source) { if ($language === null) { $language = PhutilLanguageGuesser::guessLanguage($source); } $have_pygments = !empty($this->config['pygments.enabled']); if ($language == 'php' && xhpast_is_available()) { return id(new PhutilXHPASTSyntaxHighlighter()) ->getHighlightFuture($source); } if ($language == 'console') { return id(new PhutilConsoleSyntaxHighlighter()) ->getHighlightFuture($source); } if ($language == 'diviner' || $language == 'remarkup') { return id(new PhutilDivinerSyntaxHighlighter()) ->getHighlightFuture($source); } if ($language == 'rainbow') { return id(new PhutilRainbowSyntaxHighlighter()) ->getHighlightFuture($source); } if ($language == 'php') { return id(new PhutilLexerSyntaxHighlighter()) ->setConfig('lexer', new PhutilPHPFragmentLexer()) ->setConfig('language', 'php') ->getHighlightFuture($source); } if ($have_pygments) { return id(new PhutilPygmentsSyntaxHighlighter()) ->setConfig('language', $language) ->getHighlightFuture($source); } return id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($source); } } diff --git a/src/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php b/src/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php index 47b13ca..f35445b 100644 --- a/src/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php +++ b/src/markup/syntax/engine/PhutilSyntaxHighlighterEngine.php @@ -1,37 +1,21 @@ getHighlightFuture($language, $source)->resolve(); } catch (PhutilSyntaxHighlighterException $ex) { return id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($source) ->resolve(); } } } diff --git a/src/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php b/src/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php index 287914c..abd0287 100644 --- a/src/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php +++ b/src/markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php @@ -1,43 +1,27 @@ 'php', '/x.php' => 'php', 'x.y.php' => 'php', '/x.y/z.php' => 'php', '/x.php/' => null, ); $engine = new PhutilDefaultSyntaxHighlighterEngine(); foreach ($names as $path => $language) { $detect = $engine->getLanguageFromFilename($path); $this->assertEqual($language, $detect, 'Language detect for '.$path); } } } diff --git a/src/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php index 0f269c7..c7cc239 100644 --- a/src/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php @@ -1,73 +1,57 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { $in_command = false; $lines = explode("\n", $source); foreach ($lines as $key => $line) { $matches = null; // Parse commands like this: // // some/path/ $ ./bin/example # Do things // // ...into path, command, and comment components. $pattern = '@'. ($in_command ? '()(.*?)' : '^(\S+[\\\\/] )?([$] .*?)'). '(#.*|\\\\)?$@'; if (preg_match($pattern, $line, $matches)) { $line = ''; if ($matches[1]) { $line .= phutil_escape_html($matches[1]); } $line .= ''.phutil_escape_html($matches[2]).''; if (!empty($matches[3])) { $line .= ''.phutil_escape_html($matches[3]).''; } $lines[$key] = $line; $in_command = ($matches[3] == '\\'); } else { $lines[$key] = ''.phutil_escape_html($line).''; } } $lines = implode("\n", $lines); return new ImmediateFuture($lines); } } diff --git a/src/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php index d924cc7..5133e4a 100644 --- a/src/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php @@ -1,32 +1,16 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { $source = phutil_escape_html($source); // This highlighter isn't perfect but tries to do an okay job at getting // some of the basics at least. There's lots of room for improvement. // Highlight "@{class:...}" links to other documentation pages. $source = $this->highlightPattern('/@{([\w@]+?):([^}]+?)}/', $source, 'nc'); // Highlight "@title", "@group", etc. $source = $this->highlightPattern('/^@(\w+)/m', $source, 'k'); // Highlight bold, italic and monospace. $source = $this->highlightPattern('@\\*\\*(.+?)\\*\\*@s', $source, 's'); $source = $this->highlightPattern('@(?highlightPattern( '@##([\s\S]+?)##|\B`(.+?)`\B@', $source, 's'); // Highlight stuff that looks like headers. $source = $this->highlightPattern('/^=(.*)$/m', $source, 'nv'); return new ImmediateFuture($source); } private function highlightPattern($regexp, $source, $class) { $this->replaceClass = $class; $source = preg_replace_callback( $regexp, array($this, 'replacePattern'), $source); return $source; } public function replacePattern($matches) { // NOTE: The goal here is to make sure a never crosses a newline. $content = $matches[0]; $content = explode("\n", $content); foreach ($content as $key => $line) { $content[$key] = ''. $line. ''; } return implode("\n", $content); } } diff --git a/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php index 58a952b..2b70492 100644 --- a/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php @@ -1,99 +1,83 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { $strip = false; $state = 'start'; $lang = idx($this->config, 'language'); if ($lang == 'php') { if (strpos($source, 'config, 'lexer'); $tokens = $lexer->getTokens($source, $state); $tokens = $lexer->mergeTokens($tokens); $result = array(); foreach ($tokens as $token) { list($type, $value, $context) = $token; $value = phutil_escape_html($value); $data_name = null; switch ($type) { case 'nc': case 'nf': case 'na': $data_name = ' data-symbol-name="'.$value.'"'; break; } $data_context = null; if ($context) { $context = phutil_escape_html($context); $data_context = ' data-symbol-context="'.$context.'"'; } $class_name = null; if ($type) { $class_name = ' class="'.$type.'"'; } if (strpos($value, "\n") !== false) { $value = explode("\n", $value); } else { $value = array($value); } foreach ($value as $part) { if (strlen($part)) { if ($class_name) { $result[] = ''. $part. ''; } else { $result[] = $part; } } $result[] = "\n"; } // Throw away the last "\n". array_pop($result); } $result = implode('', $result); return new ImmediateFuture($result); } } diff --git a/src/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php index 0e8787b..e9194aa 100644 --- a/src/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php @@ -1,225 +1,209 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { $language = idx($this->config, 'language'); if ($language) { $language = $this->getPygmentsLexerNameFromLanguageName($language); $future = new ExecFuture( 'pygmentize -O encoding=utf-8 -O stripnl=False -f html -l %s', $language); $scrub = false; if ($language == 'php' && strpos($source, 'write($source); return new PhutilDefaultSyntaxHighlighterEnginePygmentsFuture( $future, $source, $scrub); } return id(new PhutilDefaultSyntaxHighlighter()) ->getHighlightFuture($source); } private function getPygmentsLexerNameFromLanguageName($language) { static $map = array( 'adb' => 'ada', 'ads' => 'ada', 'ahkl' => 'ahk', 'G' => 'antlr-ruby', 'g' => 'antlr-ruby', 'htaccess' => 'apacheconf', 'as' => 'as3', 'aspx' => 'aspx-vb', 'asax' => 'aspx-vb', 'ascx' => 'aspx-vb', 'ashx' => 'aspx-vb', 'asmx' => 'aspx-vb', 'axd' => 'aspx-vb', 'sh' => 'bash', 'ksh' => 'bash', 'ebuild' => 'bash', 'eclass' => 'bash', 'cmd' => 'bat', 'bmx' => 'blitzmax', 'bf' => 'brainfuck', 'b' => 'brainfuck', 'h' => 'c', 'cfml' => 'cfm', 'cfc' => 'cfm', 'tmpl' => 'cheetah', 'spt' => 'cheetah', 'clj' => 'clojure', 'coffee' => 'coffee-script', 'cl' => 'common-lisp', 'lisp' => 'common-lisp', 'el' => 'common-lisp', 'sh-session' => 'console', 'hpp' => 'cpp', 'c++' => 'cpp', 'h++' => 'cpp', 'cc' => 'cpp', 'hh' => 'cpp', 'cxx' => 'cpp', 'hxx' => 'cpp', 'c++-objdump' => 'cpp-objdump', 'cxx-objdump' => 'cpp-objdump', 'cs' => 'csharp', 'less' => 'css', 'scss' => 'css', 'feature' => 'Cucumber', 'pyx' => 'cython', 'pxd' => 'cython', 'pxi' => 'cython', 'di' => 'd', 'pas' => 'delphi', 'patch' => 'diff', 'darcspatch' => 'dpatch', 'jbst' => 'duel', 'dyl' => 'dylan', 'erl-sh' => 'erl', 'erl' => 'erlang', 'hrl' => 'erlang', 'flx' => 'felix', 'flxh' => 'felix', 'f' => 'fortran', 'f90' => 'fortran', 's' => 'gas', 'kid' => 'genshi', 'vert' => 'glsl', 'frag' => 'glsl', 'geo' => 'glsl', 'plot' => 'gnuplot', 'plt' => 'gnuplot', 'gdc' => 'gooddata-cl', 'man' => 'groff', 'hs' => 'haskell', 'htm' => 'html', 'xhtml' => 'html', 'html' => 'html+evoque', 'phtml' => 'html+php', 'hy' => 'hybris', 'hyb' => 'hybris', 'cfg' => 'ini', 'ik' => 'ioke', 'weechatlog' => 'irc', 'll' => 'llvm', 'lgt' => 'logtalk', 'wlua' => 'lua', 'mak' => 'make', 'Makefile' => 'make', 'makefile' => 'make', 'GNUmakefile' => 'make', 'mao' => 'mako', 'mhtml' => 'mason', 'mc' => 'mason', 'mi' => 'mason', 'autohandler' => 'mason', 'dhandler' => 'mason', 'md' => 'minid', 'mo' => 'modelica', 'def' => 'modula2', 'mod' => 'modula2', 'moo' => 'moocode', 'mu' => 'mupad', 'myt' => 'myghty', 'autodelegate' => 'myghty', 'asm' => 'nasm', 'ASM' => 'nasm', 'ns2' => 'newspeak', 'm' => 'objective-c', 'mm' => 'objective-c', 'j' => 'objective-j', 'ml' => 'ocaml', 'mli' => 'ocaml', 'mll' => 'ocaml', 'mly' => 'ocaml', 'pm' => 'perl', 'ps' => 'postscript', 'eps' => 'postscript', 'po' => 'pot', 'inc' => 'pov', 'pl' => 'prolog', 'pro' => 'prolog', 'proto' => 'protobuf', 'py' => 'python', 'pyw' => 'python', 'sc' => 'python', 'SConstruct' => 'python', 'SConscript' => 'python', 'tac' => 'python', 'rl' => 'ragel-em', 'rbw' => 'rb', 'Rakefile' => 'rb', 'rake' => 'rb', 'gemspec' => 'rb', 'rbx' => 'rb', 'duby' => 'rb', 'Rout' => 'rconsole', 'r' => 'rebol', 'r3' => 'rebol', 'cw' => 'redcode', 'rest' => 'rst', 'scm' => 'scheme', 'st' => 'smalltalk', 'tpl' => 'smarty', 'S' => 'splus', 'R' => 'splus', 'hql' => 'sql', 'sqlite3-console' => 'sqlite3', 'csh' => 'tcsh', 'aux' => 'tex', 'toc' => 'tex', 'txt' => 'text', 'sv' => 'v', 'vapi' => 'vala', 'vb' => 'vb.net', 'bas' => 'vb.net', 'vm' => 'velocity', 'fhtml' => 'velocity', 'vimrc' => 'vim', 'xslt' => 'xml', 'rss' => 'xml', 'xsd' => 'xml', 'wsdl' => 'xml', 'xml' => 'xml+evoque', 'xqy' => 'xquery', 'xsl' => 'xslt', 'yml' => 'yaml', ); return idx($map, $language, $language); } } diff --git a/src/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php index f9c0ec7..9e5c1ad 100644 --- a/src/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php @@ -1,63 +1,47 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { $color = 0; $colors = array( 'rbw_r', 'rbw_o', 'rbw_y', 'rbw_g', 'rbw_b', 'rbw_i', 'rbw_v', ); $result = array(); foreach (phutil_utf8v($source) as $character) { if ($character == ' ' || $character == "\n") { $result[] = $character; continue; } $result[] = ''. phutil_escape_html($character). ''; $color = ($color + 1) % count($colors); } $result = implode('', $result); return new ImmediateFuture($result); } } diff --git a/src/markup/syntax/highlighter/PhutilSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilSyntaxHighlighter.php index e4fb164..e28cd3d 100644 --- a/src/markup/syntax/highlighter/PhutilSyntaxHighlighter.php +++ b/src/markup/syntax/highlighter/PhutilSyntaxHighlighter.php @@ -1,25 +1,9 @@ config[$key] = $value; return $this; } public function getHighlightFuture($source) { try { $result = $this->applyXHPHighlight($source); return new ImmediateFuture($result); } catch (Exception $ex) { // XHP can't highlight source that isn't syntactically valid. Fall back // to the fragment lexer. return id(new PhutilLexerSyntaxHighlighter()) ->setConfig('lexer', new PhutilPHPFragmentLexer()) ->setConfig('language', 'php') ->getHighlightFuture($source); } } private function applyXHPHighlight($source) { // We perform two passes here: one using the AST to find symbols we care // about -- particularly, class names and function names. These are used // in the crossreference stuff to link into Diffusion. After we've done our // AST pass, we do a followup pass on the token stream to catch all the // simple stuff like strings and comments. $scrub = false; if (strpos($source, 'getRootNode(); $tokens = $root->getTokens(); $interesting_symbols = $this->findInterestingSymbols($root); $out = array(); foreach ($tokens as $key => $token) { $value = phutil_escape_html($token->getValue()); $class = null; $multi = false; $attrs = ''; if (isset($interesting_symbols[$key])) { $sym = $interesting_symbols[$key]; $class = $sym[0]; if (isset($sym['context'])) { $attrs = $attrs.' data-symbol-context="'.$sym['context'].'"'; } if (isset($sym['symbol'])) { $attrs = $attrs.' data-symbol-name="'.$sym['symbol'].'"'; } } else { switch ($token->getTypeName()) { case 'T_WHITESPACE': break; case 'T_DOC_COMMENT': $class = 'dc'; $multi = true; break; case 'T_COMMENT': $class = 'c'; $multi = true; break; case 'T_CONSTANT_ENCAPSED_STRING': case 'T_ENCAPSED_AND_WHITESPACE': case 'T_INLINE_HTML': $class = 's'; $multi = true; break; case 'T_VARIABLE': $class = 'nv'; break; case 'T_OPEN_TAG': case 'T_OPEN_TAG_WITH_ECHO': case 'T_CLOSE_TAG': $class = 'o'; break; case 'T_LNUMBER': case 'T_DNUMBER': $class = 'm'; break; case 'T_STRING': static $magic = array( 'true' => true, 'false' => true, 'null' => true, ); if (isset($magic[$value])) { $class = 'k'; break; } $class = 'nx'; break; default: $class = 'k'; break; } } if ($class) { $l = ''; $r = ''; $value = $l.$value.$r; if ($multi) { // If the token may have multiple lines in it, make sure each // crosses no more than one line so the lines can be put // in a table, etc., later. $value = str_replace( "\n", $r."\n".$l, $value); } $out[] = $value; } else { $out[] = $value; } } if ($scrub) { array_shift($out); } return rtrim(implode('', $out)); } private function findInterestingSymbols(XHPASTNode $root) { // Class name symbols appear in: // class X extends X implements X, X { ... } // new X(); // $x instanceof X // catch (X $x) // function f(X $x) // X::f(); // X::$m; // X::CONST; // These are PHP builtin tokens which can appear in a classname context. // Don't link them since they don't go anywhere useful. static $builtin_class_tokens = array( 'self' => true, 'parent' => true, 'static' => true, ); // Fortunately XHPAST puts all of these in a special node type so it's // easy to find them. $result_map = array(); $class_names = $root->selectDescendantsOfType('n_CLASS_NAME'); foreach ($class_names as $class_name) { foreach ($class_name->getTokens() as $key => $token) { if (isset($builtin_class_tokens[$token->getValue()])) { // This is something like "self::method()". continue; } $result_map[$key] = array( 'nc', // "Name, Class" 'symbol' => $class_name->getConcreteString(), ); } } // Function name symbols appear in: // f() $function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL'); foreach ($function_calls as $call) { $call = $call->getChildByIndex(0); if ($call->getTypeName() == 'n_SYMBOL_NAME') { // This is a normal function call, not some $f() shenanigans. foreach ($call->getTokens() as $key => $token) { $result_map[$key] = array( 'nf', // "Name, Function" 'symbol' => $call->getConcreteString(), ); } } } // Upon encountering $x->y, link y without context, since $x is unknown. $prop_access = $root->selectDescendantsOfType('n_OBJECT_PROPERTY_ACCESS'); foreach ($prop_access as $access) { $right = $access->getChildByIndex(1); if ($right->getTypeName() == 'n_INDEX_ACCESS') { // otherwise $x->y[0] doesn't get highlighted $right = $right->getChildByIndex(0); } if ($right->getTypeName() == 'n_STRING') { foreach ($right->getTokens() as $key => $token) { $result_map[$key] = array( 'na', // "Name, Attribute" 'symbol' => $right->getConcreteString(), ); } } } // Upon encountering x::y, try to link y with context x. $static_access = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS'); foreach ($static_access as $access) { $class = $access->getChildByIndex(0); $right = $access->getChildByIndex(1); if ($class->getTypeName() == 'n_CLASS_NAME' && ($right->getTypeName() == 'n_STRING' || $right->getTypeName() == 'n_VARIABLE')) { $classname = head($class->getTokens())->getValue(); $result = array( 'na', 'symbol' => ltrim($right->getConcreteString(), '$'), ); if (!isset($builtin_class_tokens[$classname])) { $result['context'] = $classname; } foreach ($right->getTokens() as $key => $token) { $result_map[$key] = $result; } } } return $result_map; } } diff --git a/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php b/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php index 0926483..e24f195 100644 --- a/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php +++ b/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php @@ -1,44 +1,28 @@ setConfig('language', 'php'); $highlighter->setConfig('lexer', new PhutilPHPFragmentLexer()); $path = dirname(__FILE__).'/phpfragment/'; foreach (Filesystem::listDirectory($path, $include_hidden = false) as $f) { if (preg_match('/.test$/', $f)) { $expect = preg_replace('/.test$/', '.expect', $f); $source = Filesystem::readFile($path.'/'.$f); $this->assertEqual( Filesystem::readFile($path.'/'.$expect), $highlighter->getHighlightFuture($source)->resolve(), $f); } } } } diff --git a/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php b/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php index c2d0960..560179f 100644 --- a/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php +++ b/src/markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php @@ -1,46 +1,30 @@ getHighlightFuture($source); return $future->resolve(); } private function read($file) { $path = dirname(__FILE__).'/xhpast/'.$file; return Filesystem::readFile($path); } public function testBuiltinClassnames() { $this->assertEqual( $this->read('builtin-classname.expect'), $this->highlight($this->read('builtin-classname.source')), 'Builtin classnames should not be marked as linkable symbols.'); $this->assertEqual( $this->read('trailing-comment.expect'), $this->highlight($this->read('trailing-comment.source')), 'Trailing comments should not be dropped.'); } } diff --git a/src/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php b/src/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php index eba7fec..e2e2647 100644 --- a/src/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php +++ b/src/markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php @@ -1,52 +1,36 @@ source = $source; $this->scrub = $scrub; } protected function didReceiveResult($result) { list($err, $stdout, $stderr) = $result; if (!$err && strlen($stdout)) { // Strip off fluff Pygments adds. $stdout = preg_replace( '@^
(.*)
\s*$@s', '\1', $stdout); if ($this->scrub) { $stdout = preg_replace('/^.*\n/', '', $stdout); } return $stdout; } throw new PhutilSyntaxHighlighterException($stderr, $err); } } diff --git a/src/moduleutils/moduleutils.php b/src/moduleutils/moduleutils.php index f001950..1721592 100644 --- a/src/moduleutils/moduleutils.php +++ b/src/moduleutils/moduleutils.php @@ -1,70 +1,54 @@ getLibraryRoot($library); } /** * @group library */ function phutil_get_library_root_for_path($path) { foreach (Filesystem::walkToRoot($path) as $dir) { if (@file_exists($dir.'/__phutil_library_init__.php')) { return $dir; } } return null; } /** * @group library */ function phutil_get_library_name_for_root($path) { $path = rtrim(Filesystem::resolvePath($path), '/'); $bootloader = PhutilBootloader::getInstance(); $libraries = $bootloader->getAllLibraries(); foreach ($libraries as $library) { $root = $bootloader->getLibraryRoot($library); if (rtrim(Filesystem::resolvePath($root), '/') == $path) { return $library; } } return null; } /** * Warns about use of deprecated behavior. * * @group library */ function phutil_deprecated($what, $why) { PhutilErrorHandler::dispatchErrorMessage( PhutilErrorHandler::DEPRECATED, $what, array( 'why' => $why, )); } diff --git a/src/object/Phobject.php b/src/object/Phobject.php index 81e4fb3..5b57b01 100644 --- a/src/object/Phobject.php +++ b/src/object/Phobject.php @@ -1,28 +1,12 @@ line number. $map = array(); $lines = explode("\n", $text); $num = 1; foreach ($lines as $line) { $len = strlen($line) + 1; for ($jj = 0; $jj < $len; $jj++) { $map[] = $num; } ++$num; } foreach ($matches[0] as $match) { list($data, $offset) = $match; $blocks[] = array($data, $map[$offset]); } return $blocks; } public function parse($docblock) { // Strip off comments. $docblock = trim($docblock); $docblock = preg_replace('@^/\*\*@', '', $docblock); $docblock = preg_replace('@\*/$@', '', $docblock); $docblock = preg_replace('@^\s*\*@m', '', $docblock); // Normalize multi-line @specials. $lines = explode("\n", $docblock); $last = false; foreach ($lines as $k => $line) { if (preg_match('/^\s?@\w/i', $line)) { $last = $k; } else if (preg_match('/^\s*$/', $line)) { $last = false; } else if ($last !== false) { $lines[$last] = rtrim($lines[$last]).' '.trim($line); unset($lines[$k]); } } $docblock = implode("\n", $lines); $special = array(); // Parse @specials. $matches = null; $have_specials = preg_match_all( '/^\s?@([\w-]+)[ \t]*([^\n]*)/m', $docblock, $matches, PREG_SET_ORDER); if ($have_specials) { $docblock = preg_replace( '/^\s?@([\w-]+)[ \t]*([^\n]*)?\n*/m', '', $docblock); foreach ($matches as $match) { list($_, $type, $data) = $match; $data = trim($data); if (isset($special[$type])) { $special[$type] = $special[$type]."\n".$data; } else { $special[$type] = $data; } } } // For flags like "@stable" which don't have any string data, set the value // to true. foreach ($special as $type => $data) { if (!strlen(trim($data))) { $special[$type] = true; } } $docblock = str_replace("\t", ' ', $docblock); // Smush the whole docblock to the left edge. $min_indent = 80; $indent = 0; foreach (array_filter(explode("\n", $docblock)) as $line) { for ($ii = 0; $ii < strlen($line); $ii++) { if ($line[$ii] != ' ') { break; } $indent++; } $min_indent = min($indent, $min_indent); } $docblock = preg_replace( '/^'.str_repeat(' ', $min_indent).'/m', '', $docblock); $docblock = rtrim($docblock); // Trim any empty lines off the front, but leave the indent level if there // is one. $docblock = preg_replace('/^\s*\n/', '', $docblock); return array($docblock, $special); } } diff --git a/src/parser/PhutilEmailAddress.php b/src/parser/PhutilEmailAddress.php index 076837e..bc99833 100644 --- a/src/parser/PhutilEmailAddress.php +++ b/src/parser/PhutilEmailAddress.php @@ -1,105 +1,89 @@ $/', $email_address, $matches)) { $display_name = trim($matches[1], '\'" '); if (strpos($matches[2], '@') !== false) { list($local_part, $domain_name) = explode('@', $matches[2], 2); } else { $local_part = $matches[2]; $domain_name = null; } } else if (preg_match('/^(.*)@(.*)$/', $email_address, $matches)) { $display_name = null; $local_part = $matches[1]; $domain_name = $matches[2]; } else { $display_name = null; $local_part = $email_address; $domain_name = null; } $this->displayName = $display_name; $this->localPart = $local_part; $this->domainName = $domain_name; } public function __toString() { $address = $this->getAddress(); if ($this->displayName) { return $this->displayName.' <'.$address.'>'; } else { return $address; } } public function setDisplayName($display_name) { $this->displayName = $display_name; return $this; } public function getDisplayName() { return $this->displayName; } public function setLocalPart($local_part) { $this->localPart = $local_part; return $this; } public function getLocalPart() { return $this->localPart; } public function setDomainName($domain_name) { $this->domainName = $domain_name; return $this; } public function getDomainName() { return $this->domainName; } public function getAddress() { $address = $this->localPart; if ($this->domainName) { $address .= '@'.$this->domainName; } return $address; } } diff --git a/src/parser/PhutilGitURI.php b/src/parser/PhutilGitURI.php index f7448a0..e847a84 100644 --- a/src/parser/PhutilGitURI.php +++ b/src/parser/PhutilGitURI.php @@ -1,105 +1,89 @@ parseURI($uri); if ($parts) { $this->user = $parts[1]; $this->domain = $parts[2]; $this->path = $parts[3]; } } private static function parseURI($uri) { $user = '(?:([^@]+)@)?'; $domain = '([^:]+)'; $path = ':(.*)'; $regexp = '/^'.$user.$domain.$path.'$/'; $matches = null; $ok = preg_match($regexp, $uri, $matches); if ($ok) { return array_pad($matches, 4, ''); } return null; } public function __toString() { $user = null; if ($this->user) { $user = $this->user.'@'; } $domain = $this->domain; $path = $this->path; return $user.$domain.':'.$path; } public function setDomain($domain) { $this->domain = $domain; return $this; } public function getDomain() { return $this->domain; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } } diff --git a/src/parser/PhutilJSON.php b/src/parser/PhutilJSON.php index 05d5fb9..a7554f8 100644 --- a/src/parser/PhutilJSON.php +++ b/src/parser/PhutilJSON.php @@ -1,154 +1,138 @@ encodeFormattedObject($object, 0)."\n"; } /* -( Internals )---------------------------------------------------------- */ /** * Pretty-print a JSON object. * * @param dict Object to format. * @param int Current depth, for indentation. * @return string Pretty-printed value. * @task internal */ private function encodeFormattedObject($object, $depth) { if (empty($object)) { return '{}'; } $pre = $this->getIndent($depth); $key_pre = $this->getIndent($depth + 1); $keys = array(); $vals = array(); $max = 0; foreach ($object as $key => $val) { $ekey = $this->encodeFormattedValue((string)$key, 0); $max = max($max, strlen($ekey)); $keys[] = $ekey; $vals[] = $this->encodeFormattedValue($val, $depth + 1); } $key_lines = array(); foreach ($keys as $k => $key) { $key_lines[] = $key_pre.str_pad($key, $max).' : '.$vals[$k]; } $key_lines = implode(",\n", $key_lines); $out = "{\n"; $out .= $key_lines; $out .= "\n"; $out .= $pre.'}'; return $out; } /** * Pretty-print a JSON list. * * @param list List to format. * @param int Current depth, for indentation. * @return string Pretty-printed value. * @task internal */ private function encodeFormattedArray($array, $depth) { if (empty($array)) { return '[]'; } $pre = $this->getIndent($depth); $val_pre = $this->getIndent($depth + 1); $vals = array(); foreach ($array as $val) { $vals[] = $val_pre.$this->encodeFormattedValue($val, $depth + 1); } $val_lines = implode(",\n", $vals); $out = "[\n"; $out .= $val_lines; $out .= "\n"; $out .= $pre.']'; return $out; } /** * Pretty-print a JSON value. * * @param dict Value to format. * @param int Current depth, for indentation. * @return string Pretty-printed value. * @task internal */ private function encodeFormattedValue($value, $depth) { if (is_array($value)) { if (empty($value) || array_keys($value) === range(0, count($value) - 1)) { return $this->encodeFormattedArray($value, $depth); } else { return $this->encodeFormattedObject($value, $depth); } } else { return json_encode($value); } } /** * Render a string corresponding to the current indent depth. * * @param int Current depth. * @return string Indentation. * @task internal */ private function getIndent($depth) { if (!$depth) { return ''; } else { return str_repeat(' ', $depth); } } } diff --git a/src/parser/PhutilLanguageGuesser.php b/src/parser/PhutilLanguageGuesser.php index e8b51ca..cd09feb 100644 --- a/src/parser/PhutilLanguageGuesser.php +++ b/src/parser/PhutilLanguageGuesser.php @@ -1,65 +1,49 @@ 1, // Capture "#!/usr/bin/php" sorts of things. '@^#!.*bin/(\S+)@' => 1, // Capture initial " 1, // Capture emacs "mode" header. '@^.*-[*]-.*mode\s*:\s*(\S+).*-[*]-.*$@m' => 1, // Look for things that seem to be diffs. '/^---.*$\n^[+]{3}.*$\n^@@/m' => 'diff', '/^diff --git/' => 'diff', // Look for plausible console output. '@^(?:\S+[\\\\/] )?[$] @' => 'console', ); foreach ($patterns as $pattern => $language) { $matches = null; if (preg_match($pattern, $source, $matches)) { if (is_numeric($language)) { return $matches[$language]; } else { return $language; } } } return null; } } diff --git a/src/parser/PhutilSimpleOptions.php b/src/parser/PhutilSimpleOptions.php index 3a4f063..041cd4a 100644 --- a/src/parser/PhutilSimpleOptions.php +++ b/src/parser/PhutilSimpleOptions.php @@ -1,127 +1,111 @@ '4', * 'eyes' => '2', * ); * * @param string Input option list. * @return dict Parsed dictionary. * @task parse */ public static function parse($input) { $result = array(); $vars = explode(',', $input); foreach ($vars as $var) { if (strpos($var, '=') !== false) { list($key, $value) = explode('=', $var, 2); $value = trim($value); } else { list($key, $value) = array($var, true); } $key = trim($key); $key = strtolower($key); if (!self::isValidKey($key)) { // If there are bad keys, just bail, so we don't get silly results for // parsing inputs like "SELECT id, name, size FROM table". return array(); } if (!strlen($value)) { unset($result[$key]); continue; } $result[$key] = $value; } return $result; } /* -( Unparsing Simple Options )------------------------------------------- */ /** * Convert a dictionary into a simple option list. For example: * * array( * 'legs' => '4', * 'eyes' => '2', * ); * * ...becomes: * * legs=4, eyes=2 * * @param dict Input dictionary. * @return string Unparsed option list. */ public static function unparse(array $options) { $result = array(); foreach ($options as $name => $value) { if (!self::isValidKey($name)) { throw new Exception( "SimpleOptions: keys must contain only lowercase letters."); } if (!strlen($value)) { continue; } if ($value === true) { $result[] = $name; } else { $result[] = $name.'='.$value; } } return implode(', ', $result); } /* -( Internals )---------------------------------------------------------- */ private static function isValidKey($key) { return (bool)preg_match('/^[a-z]+$/', $key); } } diff --git a/src/parser/PhutilURI.php b/src/parser/PhutilURI.php index a50fe66..3a8e893 100644 --- a/src/parser/PhutilURI.php +++ b/src/parser/PhutilURI.php @@ -1,190 +1,174 @@ parseURI($uri); if ($parts) { $this->protocol = $parts[1]; $this->user = $parts[2]; $this->pass = $parts[3]; $this->domain = $parts[4]; $this->port = $parts[5]; $this->path = $parts[6]; parse_str($parts[7], $this->query); $this->fragment = $parts[8]; } } private static function parseURI($uri) { // NOTE: We allow "+" in the protocol for "svn+ssh" and similar. $protocol = '([\w+]+):\/\/'; $auth = '(?:([^:@]+)(?::([^@]+))?@)?'; $domain = '([a-zA-Z0-9\.\-_]*)'; $port = '(?::(\d+))?'; $path = '((?:\/|^)[^#?]*)?'; $query = '(?:\?([^#]*))?'; $anchor = '(?:#(.*))?'; $regexp = '/^(?:'.$protocol.$auth.$domain.$port.')?'. $path.$query.$anchor.'$/S'; $matches = null; $ok = preg_match($regexp, $uri, $matches); if ($ok) { return array_pad($matches, 9, ''); } return null; } public function __toString() { $prefix = null; if ($this->protocol || $this->domain || $this->port) { $protocol = nonempty($this->protocol, 'http'); $auth = ''; if ($this->user && $this->pass) { $auth = $this->user.':'.$this->pass.'@'; } else if ($this->user) { $auth = $this->user.'@'; } $prefix = $protocol.'://'.$auth.$this->domain; if ($this->port) { $prefix .= ':'.$this->port; } } if ($this->query) { $query = '?'.http_build_query($this->query); } else { $query = null; } if (strlen($this->getFragment())) { $fragment = '#'.$this->getFragment(); } else { $fragment = null; } return $prefix.$this->getPath().$query.$fragment; } public function setQueryParam($key, $value) { if ($value === null) { unset($this->query[$key]); } else { $this->query[$key] = $value; } return $this; } public function setQueryParams(array $params) { $this->query = $params; return $this; } public function getQueryParams() { return $this->query; } public function setProtocol($protocol) { $this->protocol = $protocol; return $this; } public function getProtocol() { return $this->protocol; } public function setDomain($domain) { $this->domain = $domain; return $this; } public function getDomain() { return $this->domain; } public function setPort($port) { $this->port = $port; return $this; } public function getPort() { return $this->port; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function setFragment($fragment) { $this->fragment = $fragment; return $this; } public function getFragment() { return $this->fragment; } public function setUser($user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function setPass($pass) { $this->pass = $pass; return $this; } public function getPass() { return $this->pass; } public function alter($key, $value) { $altered = clone $this; $altered->setQueryParam($key, $value); return $altered; } } diff --git a/src/parser/__tests__/PhutilDocblockParserTestCase.php b/src/parser/__tests__/PhutilDocblockParserTestCase.php index adc87f3..e3539ea 100644 --- a/src/parser/__tests__/PhutilDocblockParserTestCase.php +++ b/src/parser/__tests__/PhutilDocblockParserTestCase.php @@ -1,134 +1,118 @@ parseDocblock($root.$file); } } private function parseDocblock($doc_file) { $contents = Filesystem::readFile($doc_file); $file = basename($doc_file); $parser = new PhutilDocblockParser(); list($docblock, $specials) = $parser->parse($contents); switch ($file) { case 'embedded-specials.docblock': $this->assertEqual(array(), $specials); $this->assertEqual( "So long as a @special does not appear at the beginning of a line,\n". "it is parsed as normal text.", $docblock); break; case 'indented-block.docblock': $this->assertEqual(array(), $specials); $this->assertEqual( "Cozy lummox gives smart squid who asks for job pen.", $docblock); break; case 'indented-text.docblock': $this->assertEqual(array(), $specials); $this->assertEqual( "Cozy lummox gives smart squid who asks for job pen.", $docblock); break; case 'multiline-special.docblock': $this->assertEqual( array( 'special' => "x y z", ), $specials); $this->assertEqual( "", $docblock); break; case 'multi-specials.docblock': $this->assertEqual( array( 'special' => "north\nsouth", ), $specials); $this->assertEqual( "", $docblock); break; case 'specials.docblock': $this->assertEqual( array( 'type' => 'type', 'task' => 'task', ), $specials); $this->assertEqual( "", $docblock); break; case 'linebreak-breaks-specials.docblock': $this->assertEqual( array( 'title' => 'title', ), $specials); $this->assertEqual( "This is normal text, not part of the @title.", $docblock); break; case 'specials-with-hyphen.docblock': $this->assertEqual( array( 'repeat-hyphen' => "a\nb", 'multiline-hyphen' => "mmm nnn", 'normal-hyphen' => "x", ), $specials); break; case 'indented-specials.docblock': $this->assertEqual( array( 'title' => 'sendmail', ), $specials); break; case 'flag-specials.docblock': $this->assertEqual( "stuff above\n\nstuff in the middle\n\nstuff below", $docblock); $this->assertEqual( array( 'flag' => true, 'stuff' => true, 'zebra' => true, 'apple' => true, ), $specials); break; default: throw new Exception("No test case to handle file '{$file}'!"); } } } diff --git a/src/parser/__tests__/PhutilEmailAddressTestCase.php b/src/parser/__tests__/PhutilEmailAddressTestCase.php index 886a57c..9c40600 100644 --- a/src/parser/__tests__/PhutilEmailAddressTestCase.php +++ b/src/parser/__tests__/PhutilEmailAddressTestCase.php @@ -1,113 +1,97 @@ '); $this->assertEqual( 'Abraham Lincoln', $email->getDisplayName()); $this->assertEqual( 'alincoln', $email->getLocalPart()); $this->assertEqual( 'logcabin.com', $email->getDomainName()); $this->assertEqual( 'alincoln@logcabin.com', $email->getAddress()); $email = new PhutilEmailAddress('alincoln@logcabin.com'); $this->assertEqual( null, $email->getDisplayName()); $this->assertEqual( 'alincoln', $email->getLocalPart()); $this->assertEqual( 'logcabin.com', $email->getDomainName()); $this->assertEqual( 'alincoln@logcabin.com', $email->getAddress()); $email = new PhutilEmailAddress('"Abraham" '); $this->assertEqual( 'Abraham', $email->getDisplayName()); $this->assertEqual( 'alincoln', $email->getLocalPart()); $this->assertEqual( 'logcabin.com', $email->getDomainName()); $this->assertEqual( 'alincoln@logcabin.com', $email->getAddress()); $email = new PhutilEmailAddress(' alincoln@logcabin.com '); $this->assertEqual( null, $email->getDisplayName()); $this->assertEqual( 'alincoln', $email->getLocalPart()); $this->assertEqual( 'logcabin.com', $email->getDomainName()); $this->assertEqual( 'alincoln@logcabin.com', $email->getAddress()); $email = new PhutilEmailAddress('alincoln'); $this->assertEqual( null, $email->getDisplayName()); $this->assertEqual( 'alincoln', $email->getLocalPart()); $this->assertEqual( null, $email->getDomainName()); $this->assertEqual( 'alincoln', $email->getAddress()); $email = new PhutilEmailAddress('alincoln '); $this->assertEqual( 'alincoln', $email->getDisplayName()); $this->assertEqual( 'alincoln at logcabin dot com', $email->getLocalPart()); $this->assertEqual( null, $email->getDomainName()); $this->assertEqual( 'alincoln at logcabin dot com', $email->getAddress()); } } diff --git a/src/parser/__tests__/PhutilGitURITestCase.php b/src/parser/__tests__/PhutilGitURITestCase.php index b36d5ba..155a2fc 100644 --- a/src/parser/__tests__/PhutilGitURITestCase.php +++ b/src/parser/__tests__/PhutilGitURITestCase.php @@ -1,41 +1,25 @@ assertEqual('git', $uri->getUser()); $this->assertEqual('host.com', $uri->getDomain()); $this->assertEqual('path/to/something', $uri->getPath()); $this->assertEqual('git@host.com:path/to/something', (string)$uri); $uri = new PhutilGitURI('host.com:path/to/something'); $this->assertEqual('', $uri->getUser()); $this->assertEqual('host.com', $uri->getDomain()); $this->assertEqual('path/to/something', $uri->getPath()); $this->assertEqual('host.com:path/to/something', (string)$uri); } } diff --git a/src/parser/__tests__/PhutilJSONTestCase.php b/src/parser/__tests__/PhutilJSONTestCase.php index e6712d5..f6f1479 100644 --- a/src/parser/__tests__/PhutilJSONTestCase.php +++ b/src/parser/__tests__/PhutilJSONTestCase.php @@ -1,40 +1,24 @@ assertEqual( $expect, $serializer->encodeFormatted(array('x' => array())), 'Empty arrays should serialize as [], not {}.'); } } diff --git a/src/parser/__tests__/PhutilLanguageGuesserTestCase.php b/src/parser/__tests__/PhutilLanguageGuesserTestCase.php index 014ac66..96ff4fe 100644 --- a/src/parser/__tests__/PhutilLanguageGuesserTestCase.php +++ b/src/parser/__tests__/PhutilLanguageGuesserTestCase.php @@ -1,42 +1,26 @@ assertEqual( $expect, PhutilLanguageGuesser::guessLanguage($source), "Guessed language for '{$test}'."); } } } diff --git a/src/parser/__tests__/PhutilSimpleOptionsTestCase.php b/src/parser/__tests__/PhutilSimpleOptionsTestCase.php index 946008e..ee36bad 100644 --- a/src/parser/__tests__/PhutilSimpleOptionsTestCase.php +++ b/src/parser/__tests__/PhutilSimpleOptionsTestCase.php @@ -1,102 +1,86 @@ array(), // Basic parsing. 'legs=4' => array('legs' => '4'), 'legs=4,eyes=2' => array('legs' => '4', 'eyes' => '2'), // Repeated keys mean last specification wins. 'legs=4,legs=3' => array('legs' => '3'), // Keys with no value should map to true. 'flag' => array('flag' => true), 'legs=4,flag' => array('legs' => '4', 'flag' => true), // Spaces should be ignored. ' flag ' => array('flag' => true), ' legs = 4 , eyes = 2' => array('legs' => '4', 'eyes' => '2'), // Case should be ignored. 'LEGS=4' => array('legs' => '4'), 'legs=4, LEGS=4' => array('legs' => '4'), // Empty values should be absent. 'legs=' => array(), 'legs=4,legs=,eyes=2' => array('eyes' => '2'), // Strings like this should not parse as simpleoptions. 'SELECT id, name, size FROM table' => array(), ); foreach ($map as $string => $expect) { $this->assertEqual( $expect, PhutilSimpleOptions::parse($string), "Correct parse of '{$string}'"); } } public function testSimpleOptionsUnparse() { $map = array( '' => array(), 'legs=4' => array('legs' => '4'), 'legs=4, eyes=2' => array('legs' => '4', 'eyes' => '2'), 'eyes=2, legs=4' => array('eyes' => '2', 'legs' => '4'), 'legs=4, head' => array('legs' => '4', 'head' => true), 'eyes=2' => array('legs' => '', 'eyes' => '2'), ); foreach ($map as $expect => $dict) { $this->assertEqual( $expect, PhutilSimpleOptions::unparse($dict), "Correct unparse of ".print_r($dict, true)); } $bogus = array( array('LEGS' => true), array('LEGS' => 4), array('!' => '!'), array('' => '2'), ); foreach ($bogus as $bad_input) { $caught = null; try { PhutilSimpleOptions::unparse($bad_input); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, $caught instanceof Exception, "Correct throw on unparse of bad input."); } } } diff --git a/src/parser/__tests__/PhutilURITestCase.php b/src/parser/__tests__/PhutilURITestCase.php index f86ba04..3910df7 100644 --- a/src/parser/__tests__/PhutilURITestCase.php +++ b/src/parser/__tests__/PhutilURITestCase.php @@ -1,65 +1,49 @@ assertEqual('http', $uri->getProtocol(), 'protocol'); $this->assertEqual('user', $uri->getUser(), 'user'); $this->assertEqual('pass', $uri->getPass(), 'pass'); $this->assertEqual('host', $uri->getDomain(), 'domain'); $this->assertEqual('99', $uri->getPort(), 'port'); $this->assertEqual('/path/', $uri->getPath(), 'path'); $this->assertEqual( array( 'query' => 'value', ), $uri->getQueryParams(), 'query params'); $this->assertEqual('fragment', $uri->getFragment(), 'fragment'); $this->assertEqual( 'http://user:pass@host:99/path/?query=value#fragment', (string)$uri, 'uri'); $uri = new PhutilURI('ssh://git@example.com/example/example.git'); $this->assertEqual('ssh', $uri->getProtocol(), 'protocol'); $this->assertEqual('git', $uri->getUser(), 'user'); $this->assertEqual('', $uri->getPass(), 'pass'); $this->assertEqual('example.com', $uri->getDomain(), 'domain'); $this->assertEqual('', $uri->getPort(), 'port'); $this->assertEqual('/example/example.git', $uri->getPath(), 'path'); $this->assertEqual(array(), $uri->getQueryParams(), 'query params'); $this->assertEqual('', $uri->getFragment(), 'fragment'); $this->assertEqual( 'ssh://git@example.com/example/example.git', (string)$uri, 'uri'); } } diff --git a/src/parser/aast/api/AASTNode.php b/src/parser/aast/api/AASTNode.php index 543fcf8..9fa7890 100644 --- a/src/parser/aast/api/AASTNode.php +++ b/src/parser/aast/api/AASTNode.php @@ -1,278 +1,262 @@ id = $id; $this->typeID = $data[0]; if (isset($data[1])) { $this->l = $data[1]; } else { $this->l = -1; } if (isset($data[2])) { $this->r = $data[2]; } else { $this->r = -1; } $this->tree = $tree; } public function getParentNode() { return $this->parentNode; } public function getID() { return $this->id; } public function getTypeID() { return $this->typeID; } public function getTypeName() { if (empty($this->typeName)) { $this->typeName = $this->tree->getNodeTypeNameFromTypeID($this->getTypeID()); } return $this->typeName; } public function getChildren() { return $this->children; } public function getChildOfType($index, $type) { $child = $this->getChildByIndex($index); if ($child->getTypeName() != $type) { throw new Exception( "Child in position '{$index}' is not of type '{$type}': ". $this->getDescription()); } return $child; } public function getChildByIndex($index) { // NOTE: Microoptimization to avoid calls like array_values() or idx(). $idx = 0; foreach ($this->children as $child) { if ($idx == $index) { return $child; } ++$idx; } throw new Exception("No child with index '{$index}'."); } /** * Build a cache to improve the performance of selectDescendantsOfType(). This * cache makes a time/memory tradeoff by aggressively caching node * descendants. It may improve the tree's query performance substantially if * you make a large number of queries, but also requires a significant amount * of memory. * * This builds a cache for the entire tree and improves performance of all * selectDescendantsOfType() calls. */ public function buildSelectCache() { $cache = array(); foreach ($this->getChildren() as $id => $child) { $type_id = $child->getTypeID(); if (empty($cache[$type_id])) { $cache[$type_id] = array(); } $cache[$type_id][$id] = $child; foreach ($child->buildSelectCache() as $type_id => $nodes) { if (empty($cache[$type_id])) { $cache[$type_id] = array(); } $cache[$type_id] += $nodes; } } $this->selectCache = $cache; return $this->selectCache; } /** * Build a cache to improve the performance of selectTokensOfType(). This * cache makes a time/memory tradeoff by aggressively caching token types. * It may improve the tree's query performance substantially if you make a * large enumber of queries, but also requires a signficant amount of memory. * * This builds a cache for this node only. */ public function buildTokenCache() { $cache = array(); foreach ($this->getTokens() as $id => $token) { $cache[$token->getTypeName()][$id] = $token; } $this->tokenCache = $cache; return $this->tokenCache; } /** * Select all tokens of a given type. */ public function selectTokensOfType($type_name) { if (isset($this->tokenCache)) { return idx($this->tokenCache, $type_name, array()); } else { $result = array(); foreach ($this->getTokens() as $id => $token) { if ($token->getTypeName() == $type_name) { $result[$id] = $token; } } return $result; } } public function selectDescendantsOfType($type_name) { $type = $this->getTypeIDFromTypeName($type_name); if (isset($this->selectCache)) { if (isset($this->selectCache[$type])) { $nodes = $this->selectCache[$type]; } else { $nodes = array(); } } else { $nodes = $this->executeSelectDescendantsOfType($this, $type); } return AASTNodeList::newFromTreeAndNodes($this->tree, $nodes); } protected function executeSelectDescendantsOfType($node, $type) { $results = array(); foreach ($node->getChildren() as $id => $child) { if ($child->getTypeID() == $type) { $results[$id] = $child; } $results += $this->executeSelectDescendantsOfType($child, $type); } return $results; } public function getTokens() { if ($this->l == -1 || $this->r == -1) { return array(); } $tokens = $this->tree->getRawTokenStream(); $result = array(); foreach (range($this->l, $this->r) as $token_id) { $result[$token_id] = $tokens[$token_id]; } return $result; } public function getConcreteString() { $values = array(); foreach ($this->getTokens() as $token) { $values[] = $token->getValue(); } return implode('', $values); } public function getSemanticString() { $tokens = $this->getTokens(); foreach ($tokens as $id => $token) { if ($token->isComment()) { unset($tokens[$id]); } } return implode('', mpull($tokens, 'getValue')); } public function getDescription() { $concrete = $this->getConcreteString(); if (strlen($concrete) > 75) { $concrete = substr($concrete, 0, 36).'...'.substr($concrete, -36); } $concrete = addcslashes($concrete, "\\\n\""); return 'a node of type '.$this->getTypeName().': "'.$concrete.'"'; } protected function getTypeIDFromTypeName($type_name) { return $this->tree->getNodeTypeIDFromTypeName($type_name); } public function getOffset() { $stream = $this->tree->getRawTokenStream(); if (empty($stream[$this->l])) { return null; } return $stream[$this->l]->getOffset(); } public function getSurroundingNonsemanticTokens() { $before = array(); $after = array(); $tokens = $this->tree->getRawTokenStream(); if ($this->l != -1) { $before = $tokens[$this->l]->getNonsemanticTokensBefore(); } if ($this->r != -1) { $after = $tokens[$this->r]->getNonsemanticTokensAfter(); } return array($before, $after); } public function getLineNumber() { return idx($this->tree->getOffsetToLineNumberMap(), $this->getOffset()); } public function dispose() { foreach ($this->getChildren() as $child) { $child->dispose(); } unset($this->selectCache); } } diff --git a/src/parser/aast/api/AASTNodeList.php b/src/parser/aast/api/AASTNodeList.php index fb4f33b..28ef094 100644 --- a/src/parser/aast/api/AASTNodeList.php +++ b/src/parser/aast/api/AASTNodeList.php @@ -1,156 +1,140 @@ ids); } public function current() { return $this->list[$this->key()]; } public function rewind() { $this->pos = 0; } public function valid() { return $this->pos < count($this->ids); } public function next() { $this->pos++; } public function key() { return $this->ids[$this->pos]; } public static function newFromTreeAndNodes(AASTTree $tree, array $nodes) { assert_instances_of($nodes, 'AASTNode'); $obj = new AASTNodeList(); $obj->tree = $tree; $obj->list = $nodes; $obj->ids = array_keys($nodes); return $obj; } public static function newFromTree(AASTTree $tree) { $obj = new AASTNodeList(); $obj->tree = $tree; $obj->list = array(0 => $tree->getRootNode()); $obj->ids = array(0 => 0); return $obj; } protected function __construct() { } public function getDescription() { if (empty($this->list)) { return 'an empty node list'; } $desc = array(); $desc[] = "a list of ".count($this->list)." nodes:"; foreach ($this->list as $node) { $desc[] = ' '.$node->getDescription().";"; } return implode("\n", $desc); } protected function newList(array $nodes) { return AASTNodeList::newFromTreeAndNodes( $this->tree, $nodes); } public function selectDescendantsOfType($type_name) { $results = array(); foreach ($this->list as $id => $node) { $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); } return $this->newList($results); } public function selectDescendantsOfTypes(array $type_names) { $results = array(); foreach ($type_names as $type_name) { foreach ($this->list as $id => $node) { $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); } } return $this->newList($results); } public function getChildrenByIndex($index) { $results = array(); foreach ($this->list as $id => $node) { $child = $node->getChildByIndex($index); $results[$child->getID()] = $child; } return $this->newList($results); } public function add(AASTNodeList $list) { foreach ($list->list as $id => $node) { $this->list[$id] = $node; } $this->ids = array_keys($this->list); return $this; } protected function executeSelectDescendantsOfType($node, $type) { $results = array(); foreach ($node->getChildren() as $id => $child) { if ($child->getTypeID() == $type) { $results[$id] = $child; } else { $results += $this->executeSelectDescendantsOfType($child, $type); } } return $results; } public function getTokens() { $tokens = array(); foreach ($this->list as $node) { $tokens += $node->getTokens(); } return $tokens; } public function getRawNodes() { return $this->list; } } diff --git a/src/parser/aast/api/AASTToken.php b/src/parser/aast/api/AASTToken.php index 2fb4d4c..c30e429 100644 --- a/src/parser/aast/api/AASTToken.php +++ b/src/parser/aast/api/AASTToken.php @@ -1,101 +1,85 @@ id = $id; $this->typeID = $type; $this->offset = $offset; $this->value = $value; $this->tree = $tree; } public function getTokenID() { return $this->id; } public function getTypeID() { return $this->typeID; } public function getTypeName() { if (empty($this->typeName)) { $this->typeName = $this->tree->getTokenTypeNameFromTypeID($this->typeID); } return $this->typeName; } public function getValue() { return $this->value; } public function getOffset() { return $this->offset; } abstract public function isComment(); abstract public function isAnyWhitespace(); public function isSemantic() { return !($this->isComment() || $this->isAnyWhitespace()); } public function getPrevToken() { $tokens = $this->tree->getRawTokenStream(); return idx($tokens, $this->id - 1); } public function getNextToken() { $tokens = $this->tree->getRawTokenStream(); return idx($tokens, $this->id + 1); } public function getNonsemanticTokensBefore() { $tokens = $this->tree->getRawTokenStream(); $result = array(); $ii = $this->id - 1; while ($ii >= 0 && !$tokens[$ii]->isSemantic()) { $result[$ii] = $tokens[$ii]; --$ii; } return array_reverse($result); } public function getNonsemanticTokensAfter() { $tokens = $this->tree->getRawTokenStream(); $result = array(); $ii = $this->id + 1; while ($ii < count($tokens) && !$tokens[$ii]->isSemantic()) { $result[$ii] = $tokens[$ii]; ++$ii; } return $result; } } diff --git a/src/parser/aast/api/AASTTree.php b/src/parser/aast/api/AASTTree.php index bcfcb7d..8b5c132 100644 --- a/src/parser/aast/api/AASTTree.php +++ b/src/parser/aast/api/AASTTree.php @@ -1,206 +1,190 @@ stream[$ii] = $this->newToken( $ii, $token[0], substr($source, $offset, $token[1]), $offset, $this); $offset += $token[1]; ++$ii; } $this->rawSource = $source; $this->buildTree(array($tree)); } public function setTreeType($description) { $this->treeType = $description; return $this; } public function getTreeType() { return $this->treeType; } public function setTokenConstants(array $token_map) { $this->tokenConstants = $token_map; $this->tokenReverseMap = array_flip($token_map); return $this; } public function setNodeConstants(array $node_map) { $this->nodeConstants = $node_map; $this->nodeReverseMap = array_flip($node_map); return $this; } public function getNodeTypeNameFromTypeID($type_id) { if (empty($this->nodeConstants[$type_id])) { $tree_type = $this->getTreeType(); throw new Exception( "No type name for node type ID '{$type_id}' in '{$tree_type}' AAST."); } return $this->nodeConstants[$type_id]; } public function getNodeTypeIDFromTypeName($type_name) { if (empty($this->nodeReverseMap[$type_name])) { $tree_type = $this->getTreeType(); throw new Exception( "No type ID for node type name '{$type_name}' in '{$tree_type}' AAST."); } return $this->nodeReverseMap[$type_name]; } public function getTokenTypeNameFromTypeID($type_id) { if (empty($this->tokenConstants[$type_id])) { $tree_type = $this->getTreeType(); throw new Exception( "No type name for token type ID '{$type_id}' ". "in '{$tree_type}' AAST."); } return $this->tokenConstants[$type_id]; } public function getTokenTypeIDFromTypeName($type_name) { if (empty($this->tokenReverseMap[$type_name])) { $tree_type = $this->getTreeType(); throw new Exception( "No type ID for token type name '{$type_name}' ". "in '{$tree_type}' AAST."); } return $this->tokenReverseMap[$type_name]; } /** * Unlink internal datastructures so that PHP's will garbage collect the tree. * This renders the object useless. * * @return void */ public function dispose() { $this->getRootNode()->dispose(); unset($this->tree); unset($this->stream); } public function getRootNode() { return $this->tree[0]; } protected function buildTree(array $tree) { $ii = count($this->tree); $nodes = array(); foreach ($tree as $node) { $this->tree[$ii] = $this->newNode($ii, $node, $this); $nodes[$ii] = $node; ++$ii; } foreach ($nodes as $node_id => $node) { if (isset($node[3])) { $children = $this->buildTree($node[3]); foreach ($children as $child) { $child->parentNode = $this->tree[$node_id]; } $this->tree[$node_id]->children = $children; } } $result = array(); foreach ($nodes as $key => $node) { $result[$key] = $this->tree[$key]; } return $result; } public function getRawTokenStream() { return $this->stream; } public function renderAsText() { return $this->executeRenderAsText(array($this->getRootNode()), 0); } protected function executeRenderAsText($list, $depth) { $return = ''; foreach ($list as $node) { if ($depth) { $return .= str_repeat(' ', $depth); } $return .= $node->getDescription()."\n"; $return .= $this->executeRenderAsText($node->getChildren(), $depth + 1); } return $return; } public function getOffsetToLineNumberMap() { if ($this->lineMap === null) { $src = $this->rawSource; $len = strlen($src); $lno = 1; $map = array(); for ($ii = 0; $ii < $len; ++$ii) { $map[$ii] = $lno; if ($src[$ii] == "\n") { ++$lno; } } $this->lineMap = $map; } return $this->lineMap; } } diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php index 429249d..32bcb53 100644 --- a/src/parser/argument/PhutilArgumentParser.php +++ b/src/parser/argument/PhutilArgumentParser.php @@ -1,779 +1,763 @@ setTagline('make an new dog') * $args->setSynopsis(<<parse( * array( * array( * 'name' => 'name', * 'param' => 'dogname', * 'default' => 'Rover', * 'help' => 'Set the dog\'s name. By default, the dog will be '. * 'named "Rover".', * ), * array( * 'name' => 'big', * 'short' => 'b', * 'help' => 'If set, create a large dog.', * ), * )); * * $dog_name = $args->getArg('name'); * $dog_size = $args->getArg('big') ? 'big' : 'small'; * * // ... etc ... * * (For detailed documentation on supported keys in argument specifications, * see @{class:PhutilArgumentSpecification}.) * * This will handle argument parsing, and generate appropriate usage help if * the user provides an unsupported flag. @{class:PhutilArgumentParser} also * supports some builtin "standard" arguments: * * $args->parseStandardArguments(); * * See @{method:parseStandardArguments} for details. Notably, this includes * a "--help" flag, and an "--xprofile" flag for profiling command-line scripts. * * Normally, when the parser encounters an unknown flag, it will exit with * an error. However, you can use @{method:parsePartial} to consume only a * set of flags: * * $args->parsePartial($spec_list); * * This allows you to parse some flags before making decisions about other * parsing, or share some flags across scripts. The builtin standard arguments * are implemented in this way. * * There is also builtin support for "workflows", which allow you to build a * script that operates in several modes (e.g., by accepting commands like * `install`, `upgrade`, etc), like `arc` does. For detailed documentation on * workflows, see @{class:PhutilArgumentWorkflow}. * * @task parse Parsing Arguments * @task read Reading Arguments * @task help Command Help * @task internal Internals * * @group console */ final class PhutilArgumentParser { private $bin; private $argv; private $specs = array(); private $results = array(); private $parsed; private $tagline; private $synopsis; private $workflows; private $showHelp; const PARSE_ERROR_CODE = 77; /* -( Parsing Arguments )-------------------------------------------------- */ /** * Build a new parser. Generally, you start a script with: * * $args = new PhutilArgumentParser($argv); * * @param list Argument vector to parse, generally the $argv global. * @task parse */ public function __construct(array $argv) { $this->bin = $argv[0]; $this->argv = array_slice($argv, 1); } /** * Parse and consume a list of arguments, removing them from the argument * vector but leaving unparsed arguments for later consumption. You can * retreive unconsumed arguments directly with * @{method:getUnconsumedArgumentVector}. Doing a partial parse can make it * easier to share common flags across scripts or workflows. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parsePartial(array $specs) { $specs = PhutilArgumentSpecification::newSpecsFromList($specs); $this->mergeSpecs($specs); $specs_by_name = mpull($specs, null, 'getName'); $specs_by_short = mpull($specs, null, 'getShortAlias'); unset($specs_by_short[null]); $argv = $this->argv; $len = count($argv); for ($ii = 0; $ii < $len; $ii++) { $arg = $argv[$ii]; $map = null; if ($arg == '--') { // This indicates "end of flags". break; } else if ($arg == '-') { // This is a normal argument (e.g., stdin). continue; } else if (!strncmp('--', $arg, 2)) { $pre = '--'; $arg = substr($arg, 2); $map = $specs_by_name; } else if (!strncmp('-', $arg, 1) && strlen($arg) > 1) { $pre = '-'; $arg = substr($arg, 1); $map = $specs_by_short; } if ($map) { $val = null; $parts = explode('=', $arg, 2); if (count($parts) == 2) { list($arg, $val) = $parts; } if (isset($map[$arg])) { $spec = $map[$arg]; unset($argv[$ii]); $param_name = $spec->getParamName(); if ($val !== null) { if ($param_name === null) { throw new PhutilArgumentUsageException( "Argument '{$pre}{$arg}' does not take a parameter."); } } else { if ($param_name !== null) { if ($ii + 1 < $len) { $val = $argv[$ii + 1]; unset($argv[$ii + 1]); $ii++; } else { throw new PhutilArgumentUsageException( "Argument '{$pre}{$arg}' requires a parameter."); } } else { $val = true; } } if (!$spec->getRepeatable()) { if (array_key_exists($spec->getName(), $this->results)) { throw new PhutilArgumentUsageException( "Argument '{$pre}{$arg}' was provided twice."); } } $conflicts = $spec->getConflicts(); foreach ($conflicts as $conflict => $reason) { if (array_key_exists($conflict, $this->results)) { if (!is_string($reason) || !strlen($reason)) { $reason = '.'; } else { $reason = ': '.$reason.'.'; } throw new PhutilArgumentUsageException( "Argument '{$pre}{$arg}' conflicts with argument ". "'--{$conflict}'{$reason}"); } } if ($spec->getRepeatable()) { if ($spec->getParamName() === null) { if (empty($this->results[$spec->getName()])) { $this->results[$spec->getName()] = 0; } $this->results[$spec->getName()]++; } else { $this->results[$spec->getName()][] = $val; } } else { $this->results[$spec->getName()] = $val; } } } } foreach ($specs as $spec) { if ($spec->getWildcard()) { $this->results[$spec->getName()] = $this->filterWildcardArgv($argv); $argv = array(); break; } } $this->argv = array_values($argv); return $this; } /** * Parse and consume a list of arguments, throwing an exception if there is * anything left unconsumed. This is like @{method:parsePartial}, but raises * a {class:PhutilArgumentUsageException} if there are leftovers. * * Normally, you would call @{method:parse} instead, which emits a * user-friendly error. You can also use @{method:printUsageException} to * render the exception in a user-friendly way. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parseFull(array $specs) { $this->parsePartial($specs); if (count($this->argv)) { $arg = head($this->argv); throw new PhutilArgumentUsageException( "Unrecognized argument '{$arg}'."); } if ($this->showHelp) { $this->printHelpAndExit(); } return $this; } /** * Parse and consume a list of arguments, raising a user-friendly error if * anything remains. See also @{method:parseFull} and @{method:parsePartial}. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parse(array $specs) { try { return $this->parseFull($specs); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } } /** * Parse and execute workflows, raising a user-friendly error if anything * remains. See also @{method:parseWorkflowsFull}. * * See @{class:PhutilArgumentWorkflow} for details on using workflows. * * @param list List of argument specs, see * @{class:PhutilArgumentSpecification}. * @return this * @task parse */ public function parseWorkflows(array $workflows) { try { return $this->parseWorkflowsFull($workflows); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } } /** * Select a workflow. For commands that may operate in several modes, like * `arc`, the modes can be split into "workflows". Each workflow specifies * the arguments it accepts. This method takes a list of workflows, selects * the chosen workflow, parses its arguments, and either executes it (if it * is executable) or returns it for handling. * * See @{class:PhutilArgumentWorkflow} for details on using workflows. * * @param list List of @{class:PhutilArgumentWorkflow}s. * @return PhutilArgumentWorkflow|no Returns the chosen workflow if it is * not executable, or executes it and * exits with a return code if it is. * @task parse */ public function parseWorkflowsFull(array $workflows) { assert_instances_of($workflows, 'PhutilArgumentWorkflow'); // Clear out existing workflows. We need to do this to permit the // construction of sub-workflows. $this->workflows = array(); foreach ($workflows as $workflow) { $name = $workflow->getName(); if ($name === null) { throw new PhutilArgumentSpecificationException( "Workflow has no name!"); } if (isset($this->workflows[$name])) { throw new PhutilArgumentSpecificationException( "Two workflows with name '{$name}!"); } $this->workflows[$name] = $workflow; } $argv = $this->argv; if (empty($argv)) { // TODO: this is kind of hacky / magical. if (isset($this->workflows['help'])) { $argv = array('help'); } else { throw new PhutilArgumentUsageException( "No workflow selected."); } } $flow = array_shift($argv); $flow = strtolower($flow); if (empty($this->workflows[$flow])) { throw new PhutilArgumentUsageException( "Invalid workflow '{$flow}'."); } $workflow = $this->workflows[$flow]; if ($this->showHelp) { $this->printHelpAndExit(); } $this->argv = array_values($argv); if ($workflow->shouldParsePartial()) { $this->parsePartial($workflow->getArguments()); } else { $this->parse($workflow->getArguments()); } if ($workflow->isExecutable()) { $err = $workflow->execute($this); exit($err); } else { return $workflow; } } /** * Parse "standard" arguments and apply their effects: * * --trace Enable service call tracing. * --no-ansi Disable ANSI color/style sequences. * --xprofile Write out an XHProf profile. * --help Show help. * * @return this * * @phutil-external-symbol function xhprof_enable */ public function parseStandardArguments() { try { $this->parsePartial( array( array( 'name' => 'trace', 'help' => 'Trace command execution and show service calls.', 'standard' => true, ), array( 'name' => 'no-ansi', 'help' => 'Disable ANSI terminal codes, printing plain text with '. 'no color or style.', 'conflicts' => array( 'ansi' => null, ), 'standard' => true, ), array( 'name' => 'ansi', 'help' => "Use formatting even in environments which probably ". "don't support it.", 'standard' => true, ), array( 'name' => 'xprofile', 'param' => 'profile', 'help' => 'Profile script execution and write results to a file.', 'standard' => true, ), array( 'name' => 'help', 'short' => 'h', 'help' => 'Show this help.', 'standard' => true, ), array( 'name' => 'show-standard-options', 'help' => 'Show every option, including standard options '. 'like this one.', 'standard' => true, ), array( 'name' => 'recon', 'help' => 'Start in remote console mode.', 'standard' => true, ), )); } catch (PhutilArgumentUsageException $ex) { $this->printUsageException($ex); exit(self::PARSE_ERROR_CODE); } if ($this->getArg('trace')) { PhutilServiceProfiler::installEchoListener(); } if ($this->getArg('no-ansi')) { PhutilConsoleFormatter::disableANSI(true); } if ($this->getArg('ansi')) { PhutilConsoleFormatter::disableANSI(false); } if ($this->getArg('help')) { $this->showHelp = true; } $xprofile = $this->getArg('xprofile'); if ($xprofile) { if (!function_exists('xhprof_enable')) { throw new Exception("To use '--xprofile', you must install XHProf."); } xhprof_enable(0); register_shutdown_function(array($this, 'shutdownProfiler')); } $recon = $this->getArg('recon'); if ($recon) { $remote_console = PhutilConsole::newRemoteConsole(); $remote_console->beginRedirectOut(); PhutilConsole::setConsole($remote_console); } else if ($this->getArg('trace')) { $server = new PhutilConsoleServer(); $server->setEnableLog(true); $console = PhutilConsole::newConsoleForServer($server); PhutilConsole::setConsole($console); } return $this; } /* -( Reading Arguments )-------------------------------------------------- */ public function getArg($name) { if (empty($this->specs[$name])) { throw new PhutilArgumentSpecificationException( "No specification exists for argument '{$name}'!"); } if (idx($this->results, $name) !== null) { return $this->results[$name]; } return $this->specs[$name]->getDefault(); } public function getUnconsumedArgumentVector() { return $this->argv; } /* -( Command Help )------------------------------------------------------- */ public function setSynopsis($synopsis) { $this->synopsis = $synopsis; return $this; } public function setTagline($tagline) { $this->tagline = $tagline; return $this; } public function printHelpAndExit() { echo $this->renderHelp(); exit(self::PARSE_ERROR_CODE); } public function renderHelp() { $out = array(); $more = array(); if ($this->bin) { $out[] = $this->format('**NAME**'); $name = $this->indent(6, '**%s**', basename($this->bin)); if ($this->tagline) { $name .= $this->format(' - '.$this->tagline); } $out[] = $name; $out[] = null; } if ($this->synopsis) { $out[] = $this->format('**SYNOPSIS**'); $out[] = $this->indent(6, $this->synopsis); $out[] = null; } if ($this->workflows) { $has_help = false; $out[] = $this->format('**WORKFLOWS**'); $out[] = null; $flows = $this->workflows; ksort($flows); foreach ($flows as $workflow) { if ($workflow->getName() == 'help') { $has_help = true; } $out[] = $this->renderWorkflowHelp( $workflow->getName(), $show_flags = false); } if ($has_help) { $more[] = "Use **help** __command__ for a detailed command reference."; } } $specs = $this->renderArgumentSpecs($this->specs); if ($specs) { $out[] = $this->format('**OPTION REFERENCE**'); $out[] = null; $out[] = $specs; } // If we have standard options but no --show-standard-options, print out // a quick hint about it. if (!empty($this->specs['show-standard-options']) && !$this->getArg('show-standard-options')) { $more[] = "Use __--show-standard-options__ to show additional options."; } $out[] = null; if ($more) { foreach ($more as $hint) { $out[] = $this->indent(0, $hint); } $out[] = null; } return implode("\n", $out); } public function renderWorkflowHelp( $workflow_name, $show_flags = false) { $out = array(); $workflow = idx($this->workflows, strtolower($workflow_name)); if (!$workflow) { $out[] = $this->indent( 6, "There is no **{$workflow_name}** workflow."); } else { $out[] = $this->indent(6, $workflow->getExamples()); $out[] = $this->indent(6, $workflow->getSynopsis()); if ($show_flags) { $specs = $this->renderArgumentSpecs($workflow->getArguments()); if ($specs) { $out[] = null; $out[] = $specs; } } } $out[] = null; return implode("\n", $out); } public function printUsageException(PhutilArgumentUsageException $ex) { fwrite( STDERR, $this->format("**Usage Exception:** %s\n", $ex->getMessage())); } /* -( Internals )---------------------------------------------------------- */ private function filterWildcardArgv(array $argv) { foreach ($argv as $key => $value) { if ($value == '--') { unset($argv[$key]); break; } else if (!strncmp($value, '-', 1) && strlen($value) > 1) { throw new PhutilArgumentUsageException( "Argument '{$value}' is unrecognized. Use '--' to indicate the ". "end of flags."); } } return array_values($argv); } private function mergeSpecs(array $specs) { $short_map = mpull($this->specs, null, 'getShortAlias'); unset($short_map[null]); $wildcard = null; foreach ($this->specs as $spec) { if ($spec->getWildcard()) { $wildcard = $spec; break; } } foreach ($specs as $spec) { $spec->validate(); $name = $spec->getName(); if (isset($this->specs[$name])) { throw new PhutilArgumentSpecificationException( "Two argument specifications have the same name ('{$name}')."); } $short = $spec->getShortAlias(); if ($short) { if (isset($short_map[$short])) { throw new PhutilArgumentSpecificationException( "Two argument specifications have the same short alias ". "('{$short}')."); } $short_map[$short] = $spec; } if ($spec->getWildcard()) { if ($wildcard) { throw new PhutilArgumentSpecificationException( "Two argument specifications are marked as wildcard arguments. ". "You can have a maximum of one wildcard argument."); } else { $wildcard = $spec; } } $this->specs[$name] = $spec; } foreach ($this->specs as $name => $spec) { foreach ($spec->getConflicts() as $conflict => $reason) { if (empty($this->specs[$conflict])) { throw new PhutilArgumentSpecificationException( "Argument '{$name}' conflicts with unspecified argument ". "'{$conflict}'."); } if ($conflict == $name) { throw new PhutilArgumentSpecificationException( "Argument '{$name}' conflicts with itself!"); } } } } private function renderArgumentSpecs(array $specs) { foreach ($specs as $key => $spec) { if ($spec->getWildcard()) { unset($specs[$key]); } } $out = array(); $specs = msort($specs, 'getName'); foreach ($specs as $spec) { if ($spec->getStandard() && !$this->getArg('show-standard-options')) { // If this is a standard argument and the user didn't pass // --show-standard-options, skip it. continue; } $name = $this->indent(6, '__--%s__', $spec->getName()); $short = null; if ($spec->getShortAlias()) { $short = $this->format(', __-%s__', $spec->getShortAlias()); } if ($spec->getParamName()) { $param = $this->format(' __%s__', $spec->getParamName()); $name .= $param; if ($short) { $short .= $param; } } $out[] = $name.$short; $out[] = $this->indent(10, $spec->getHelp()); $out[] = null; } return implode("\n", $out); } private function format($str /* , ... */) { $args = func_get_args(); return call_user_func_array( 'phutil_console_format', $args); } private function indent($level, $str /* , ... */) { $args = func_get_args(); $args = array_slice($args, 1); $text = call_user_func_array(array($this, 'format'), $args); return phutil_console_wrap($text, $level); } /** * @phutil-external-symbol function xhprof_disable */ public function shutdownProfiler() { $data = xhprof_disable(); $data = serialize($data); Filesystem::writeFile($this->getArg('xprofile'), $data); } } diff --git a/src/parser/argument/PhutilArgumentSpecification.php b/src/parser/argument/PhutilArgumentSpecification.php index 3acc6b3..dc34f49 100644 --- a/src/parser/argument/PhutilArgumentSpecification.php +++ b/src/parser/argument/PhutilArgumentSpecification.php @@ -1,277 +1,261 @@ 'verbose', * 'short' => 'v', * )); * * Recognized keys and equivalent verbose methods are: * * name setName() * help setHelp() * short setShortAlias() * param setParamName() * default setDefault() * conflicts setConflicts() * wildcard setWildcard() * repeat setRepeatable() * * @param dict Dictionary of quick parameter definitions. * @return PhutilArgumentSpecification Constructed argument specification. */ public static function newQuickSpec(array $spec) { $recognized_keys = array( 'name', 'help', 'short', 'param', 'default', 'conflicts', 'wildcard', 'repeat', 'standard', ); $unrecognized = array_diff_key( $spec, array_fill_keys($recognized_keys, true)); foreach ($unrecognized as $key => $ignored) { throw new PhutilArgumentSpecificationException( "Unrecognized key '{$key}' in argument specification. Recognized keys ". "are: ".implode(', ', $recognized_keys).'.'); } $obj = new PhutilArgumentSpecification(); foreach ($spec as $key => $value) { switch ($key) { case 'name': $obj->setName($value); break; case 'help': $obj->setHelp($value); break; case 'short': $obj->setShortAlias($value); break; case 'param': $obj->setParamName($value); break; case 'default': $obj->setDefault($value); break; case 'conflicts': $obj->setConflicts($value); break; case 'wildcard': $obj->setWildcard($value); break; case 'repeat': $obj->setRepeatable($value); break; case 'standard': $obj->setStandard($value); break; } } $obj->validate(); return $obj; } public static function newSpecsFromList(array $specs) { foreach ($specs as $key => $spec) { if (is_array($spec)) { $specs[$key] = PhutilArgumentSpecification::newQuickSpec( $spec); } } return $specs; } public function setName($name) { self::validateName($name); $this->name = $name; return $this; } private static function validateName($name) { if (!preg_match('/^[a-z0-9][a-z0-9-]*$/', $name)) { throw new PhutilArgumentSpecificationException( "Argument names may only contain a-z, 0-9 and -, and must be ". "at least one character long. '{$name}' is invalid."); } } public function getName() { return $this->name; } public function setHelp($help) { $this->help = $help; return $this; } public function getHelp() { return $this->help; } public function setShortAlias($short_alias) { self::validateShortAlias($short_alias); $this->shortAlias = $short_alias; return $this; } private static function validateShortAlias($alias) { if (strlen($alias) !== 1) { throw new PhutilArgumentSpecificationException( "Argument short aliases must be exactly one character long. ". "'{$alias}' is invalid."); } if (!preg_match('/^[a-zA-Z0-9]$/', $alias)) { throw new PhutilArgumentSpecificationException( "Argument short aliases may only be in a-z, A-Z and 0-9. ". "'{$alias}' is invalid."); } } public function getShortAlias() { return $this->shortAlias; } public function setParamName($param_name) { $this->paramName = $param_name; return $this; } public function getParamName() { return $this->paramName; } public function setDefault($default) { $this->default = $default; return $this; } public function getDefault() { if ($this->getParamName() === null) { if ($this->getRepeatable()) { return 0; } else { return false; } } else { if ($this->getRepeatable()) { return array(); } else { return $this->default; } } } public function setConflicts(array $conflicts) { $this->conflicts = $conflicts; return $this; } public function getConflicts() { return $this->conflicts; } public function setWildcard($wildcard) { $this->wildcard = $wildcard; return $this; } public function getWildcard() { return $this->wildcard; } public function setRepeatable($repeatable) { $this->repeatable = $repeatable; return $this; } public function getRepeatable() { return $this->repeatable; } public function setStandard($standard) { $this->standard = $standard; return $this; } public function getStandard() { return $this->standard; } public function validate() { if ($this->name === null) { throw new PhutilArgumentSpecificationException( "Argument specification MUST have a 'name'."); } if ($this->getWildcard()) { if ($this->getParamName()) { throw new PhutilArgumentSpecificationException( "Wildcard arguments may not specify a parameter."); } if ($this->getRepeatable()) { throw new PhutilArgumentSpecificationException( "Wildcard arguments may not be repeatable."); } } if ($this->default !== null) { if ($this->getRepeatable()) { throw new PhutilArgumentSpecificationException( "Repeatable arguments may not have a default (always array() for ". "arguments which accept a parameter, or 0 for arguments which do ". "not)."); } else if ($this->getParamName() === null) { throw new PhutilArgumentSpecificationException( "Flag arguments may not have a default (always false)."); } } } } diff --git a/src/parser/argument/__tests__/PhutilArgumentParserTestCase.php b/src/parser/argument/__tests__/PhutilArgumentParserTestCase.php index 62d82be..0ccd750 100644 --- a/src/parser/argument/__tests__/PhutilArgumentParserTestCase.php +++ b/src/parser/argument/__tests__/PhutilArgumentParserTestCase.php @@ -1,425 +1,409 @@ 'flag', )); $args = new PhutilArgumentParser(array('bin')); $args->parseFull($specs); $this->assertEqual(false, $args->getArg('flag')); $args = new PhutilArgumentParser(array('bin', '--flag')); $args->parseFull($specs); $this->assertEqual(true, $args->getArg('flag')); } public function testWildcards() { $specs = array( array( 'name' => 'flag', ), array( 'name' => 'files', 'wildcard' => true, ), ); $args = new PhutilArgumentParser(array('bin', '--flag', 'a', 'b')); $args->parseFull($specs); $this->assertEqual(true, $args->getArg('flag')); $this->assertEqual( array('a', 'b'), $args->getArg('files')); $caught = null; try { $args = new PhutilArgumentParser(array('bin', '--derp', 'a', 'b')); $args->parseFull($specs); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); $args = new PhutilArgumentParser(array('bin', '--', '--derp', 'a', 'b')); $args->parseFull($specs); $this->assertEqual( array('--derp', 'a', 'b'), $args->getArg('files')); } public function testPartialParse() { $specs = array( array( 'name' => 'flag', ), ); $args = new PhutilArgumentParser(array('bin', 'a', '--flag', '--', 'b')); $args->parsePartial($specs); $this->assertEqual( array('a', '--', 'b'), $args->getUnconsumedArgumentVector()); } public function testBadArg() { $args = new PhutilArgumentParser(array('bin')); $args->parseFull(array()); $caught = null; try { $args->getArg('flag'); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicateNames() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parseFull( array( array( 'name' => 'x', ), array( 'name' => 'x', ))); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicateNamesWithParsePartial() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parsePartial( array( array( 'name' => 'x', ))); $args->parsePartial( array( array( 'name' => 'x', ))); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicateShortAliases() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parseFull( array( array( 'name' => 'x', 'short' => 'x', ), array( 'name' => 'y', 'short' => 'x', ))); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicateWildcards() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parseFull( array( array( 'name' => 'x', 'wildcard' => true, ), array( 'name' => 'y', 'wildcard' => true, ))); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicatePartialWildcards() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parsePartial( array( array( 'name' => 'x', 'wildcard' => true, ), )); $args->parsePartial( array( array( 'name' => 'y', 'wildcard' => true, ), )); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testConflictSpecificationWithUnrecognizedArg() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parseFull( array( array( 'name' => 'x', 'conflicts' => array( 'y' => true, ), ), )); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testConflictSpecificationWithSelf() { $args = new PhutilArgumentParser(array('bin')); $caught = null; try { $args->parseFull( array( array( 'name' => 'x', 'conflicts' => array( 'x' => true, ), ), )); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testUnrecognizedFlag() { $args = new PhutilArgumentParser(array('bin', '--flag')); $caught = null; try { $args->parseFull(array()); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testDuplicateFlag() { $args = new PhutilArgumentParser(array('bin', '--flag', '--flag')); $caught = null; try { $args->parseFull( array( array( 'name' => 'flag', ), )); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testMissingParameterValue() { $args = new PhutilArgumentParser(array('bin', '--with')); $caught = null; try { $args->parseFull( array( array( 'name' => 'with', 'param' => 'stuff', ), )); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testExtraParameterValue() { $args = new PhutilArgumentParser(array('bin', '--true=apple')); $caught = null; try { $args->parseFull( array( array( 'name' => 'true', ), )); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testConflictParameterValue() { $args = new PhutilArgumentParser(array('bin', '--true', '--false')); $caught = null; try { $args->parseFull( array( array( 'name' => 'true', 'conflicts' => array( 'false' => true, ), ), array( 'name' => 'false', 'conflicts' => array( 'true' => true, ), ), )); } catch (PhutilArgumentUsageException $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testParameterValues() { $specs = array( array( 'name' => 'a', 'param' => 'value', ), array( 'name' => 'b', 'param' => 'value', ), array( 'name' => 'cee', 'short' => 'c', 'param' => 'value', ), array( 'name' => 'dee', 'short' => 'd', 'param' => 'value', ), ); $args = new PhutilArgumentParser( array( 'bin', '--a', 'a', '--b=b', '-c', 'c', '-d=d', )); $args->parseFull($specs); $this->assertEqual('a', $args->getArg('a')); $this->assertEqual('b', $args->getArg('b')); $this->assertEqual('c', $args->getArg('cee')); $this->assertEqual('d', $args->getArg('dee')); } public function testStdinValidParameter() { $specs = array( array( 'name' => 'file', 'param' => 'file', ), ); $args = new PhutilArgumentParser( array( 'bin', '-', '--file', '-', )); $args->parsePartial($specs); $this->assertEqual('-', $args->getArg('file')); } public function testRepeatableFlag() { $specs = array( array( 'name' => 'verbose', 'short' => 'v', 'repeat' => true, ), ); $args = new PhutilArgumentParser(array('bin', '-v', '-v', '-v')); $args->parseFull($specs); $this->assertEqual(3, $args->getArg('verbose')); } public function testRepeatableParam() { $specs = array( array( 'name' => 'eat', 'param' => 'fruit', 'repeat' => true, ), ); $args = new PhutilArgumentParser(array( 'bin', '--eat', 'apple', '--eat', 'pear', '--eat=orange', )); $args->parseFull($specs); $this->assertEqual( array('apple', 'pear', 'orange'), $args->getArg('eat')); } } diff --git a/src/parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php b/src/parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php index 482e16e..95cd46a 100644 --- a/src/parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php +++ b/src/parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php @@ -1,161 +1,145 @@ true, 'xx' => true, '!' => false, 'XX' => false, '1=' => false, '--' => false, 'no-stuff' => true, '-stuff' => false, ); foreach ($names as $name => $valid) { $caught = null; try { PhutilArgumentSpecification::newQuickSpec( array( 'name' => $name, )); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual( !$valid, $caught instanceof Exception, "Arg name '{$name}'."); } } public function testAliases() { $aliases = array( 'a' => true, '1' => true, 'no' => false, '-' => false, '_' => false, ' ' => false, '' => false, ); foreach ($aliases as $alias => $valid) { $caught = null; try { PhutilArgumentSpecification::newQuickSpec( array( 'name' => 'example', 'short' => $alias, )); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual( !$valid, $caught instanceof Exception, "Arg alias '{$alias}'."); } } public function testSpecs() { $good_specs = array( array( 'name' => 'verbose', ), array( 'name' => 'verbose', 'short' => 'v', 'help' => 'Derp.', 'param' => 'level', 'default' => 'y', 'conflicts' => array( 'quiet' => true, ), 'wildcard' => false, ), array( 'name' => 'files', 'wildcard' => true, ), ); $bad_specs = array( array( ), array( 'alias' => 'v', ), array( 'name' => 'derp', 'fruit' => 'apple', ), array( 'name' => 'x', 'default' => 'y', ), array( 'name' => 'x', 'param' => 'y', 'default' => 'z', 'repeat' => true, ), array( 'name' => 'x', 'wildcard' => true, 'repeat' => true, ), array( 'name' => 'x', 'param' => 'y', 'wildcard' => true, ), ); $cases = array( array(true, $good_specs), array(false, $bad_specs), ); foreach ($cases as $case) { list($expect, $specs) = $case; foreach ($specs as $spec) { $caught = null; try { PhutilArgumentSpecification::newQuickSpec($spec); } catch (PhutilArgumentSpecificationException $ex) { $caught = $ex; } $this->assertEqual( !$expect, $caught instanceof Exception, "Spec validity for: ".print_r($spec, true)); } } } } diff --git a/src/parser/argument/exception/PhutilArgumentParserException.php b/src/parser/argument/exception/PhutilArgumentParserException.php index 0810ddd..8dc1290 100644 --- a/src/parser/argument/exception/PhutilArgumentParserException.php +++ b/src/parser/argument/exception/PhutilArgumentParserException.php @@ -1,24 +1,8 @@ setTagline('simple calculator example'); * $args->setSynopsis(<<setName('add') * ->setExamples('**add** __n__ ...') * ->setSynopsis('Compute the sum of a list of numbers.') * ->setArguments( * array( * array( * 'name' => 'numbers', * 'wildcard' => true, * ), * )); * * $mul_workflow = id(new PhutilArgumentWorkflow()) * ->setName('mul') * ->setExamples('**mul** __n__ ...') * ->setSynopsis('Compute the product of a list of numbers.') * ->setArguments( * array( * array( * 'name' => 'numbers', * 'wildcard' => true, * ), * )); * * $flow = $args->parseWorkflows( * array( * $add_workflow, * $mul_workflow, * new PhutilHelpArgumentWorkflow(), * )); * * $nums = $args->getArg('numbers'); * if (empty($nums)) { * echo "You must provide one or more numbers!\n"; * exit(1); * } * * foreach ($nums as $num) { * if (!is_numeric($num)) { * echo "Number '{$num}' is not numeric!\n"; * exit(1); * } * } * * switch ($flow->getName()) { * case 'add': * echo array_sum($nums)."\n"; * break; * case 'mul': * echo array_product($nums)."\n"; * break; * } * * You can also subclass this class and return `true` from * @{method:isExecutable}. In this case, the parser will automatically select * your workflow when the user invokes it. * * @stable * @concrete-extensible * @group console */ class PhutilArgumentWorkflow { private $name; private $synopsis; private $specs = array(); private $examples; final public function __construct() { $this->didConstruct(); } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } final public function setExamples($examples) { $this->examples = $examples; return $this; } final public function getExamples() { if (!$this->examples) { return "**".$this->name."**"; } return $this->examples; } final public function setSynopsis($synopsis) { $this->synopsis = $synopsis; return $this; } final public function getSynopsis() { return $this->synopsis; } final public function setArguments(array $specs) { $specs = PhutilArgumentSpecification::newSpecsFromList($specs); $this->specs = $specs; return $this; } final public function getArguments() { return $this->specs; } protected function didConstruct() { return null; } public function isExecutable() { return false; } public function execute(PhutilArgumentParser $args) { throw new Exception("This workflow isn't executable!"); } /** * Normally, workflow arguments are parsed fully, so unexpected arguments will * raise an error. You can return `true` from this method to parse workflow * arguments only partially. This will allow you to manually parse remaining * arguments or delegate to a second level of workflows. * * @return bool True to partially parse workflow arguments (default false). */ public function shouldParsePartial() { return false; } } diff --git a/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php b/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php index d131539..1f2a210 100644 --- a/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php +++ b/src/parser/argument/workflow/PhutilHelpArgumentWorkflow.php @@ -1,63 +1,47 @@ setName('help'); $this->setExamples(<<setSynopsis(<<setArguments( array( array( 'name' => 'help-with-what', 'wildcard' => true, ))); } public function isExecutable() { return true; } public function execute(PhutilArgumentParser $args) { $with = $args->getArg('help-with-what'); if (!$with) { $args->printHelpAndExit(); } else { foreach ($with as $thing) { echo phutil_console_format( "**%s WORKFLOW**\n\n", strtoupper($thing)); echo $args->renderWorkflowHelp($thing, $show_flags = true); echo "\n"; } exit(PhutilArgumentParser::PARSE_ERROR_CODE); } } } diff --git a/src/parser/xhpast/api/XHPASTNode.php b/src/parser/xhpast/api/XHPASTNode.php index 2f64d1e..2202bf1 100644 --- a/src/parser/xhpast/api/XHPASTNode.php +++ b/src/parser/xhpast/api/XHPASTNode.php @@ -1,190 +1,174 @@ getTypeName() == 'n_STRING_SCALAR' || $this->getTypeName() == 'n_NUMERIC_SCALAR'); } public function getDocblockToken() { if ($this->l == -1) { return null; } $tokens = $this->tree->getRawTokenStream(); for ($ii = $this->l - 1; $ii >= 0; $ii--) { if ($tokens[$ii]->getTypeName() == 'T_DOC_COMMENT') { return $tokens[$ii]; } if (!$tokens[$ii]->isAnyWhitespace()) { return null; } } return null; } public function evalStatic() { switch ($this->getTypeName()) { case 'n_STATEMENT': return $this->getChildByIndex(0)->evalStatic(); break; case 'n_STRING_SCALAR': return (string)$this->getStringLiteralValue(); case 'n_NUMERIC_SCALAR': $value = $this->getSemanticString(); if (preg_match('/^0x/i', $value)) { // Hex return (int)base_convert(substr($value, 2), 16, 10); } else if (preg_match('/^0\d+$/i', $value)) { // Octal return (int)base_convert(substr($value, 1), 8, 10); } else if (preg_match('/^\d+$/', $value)) { return (int)$value; } else { return (double)$value; } break; case 'n_SYMBOL_NAME': $value = $this->getSemanticString(); if ($value == 'INF') { return INF; } switch (strtolower($value)) { case 'true': return true; case 'false': return false; case 'null': return null; default: throw new Exception('Unrecognized symbol name.'); } break; case 'n_UNARY_PREFIX_EXPRESSION': $operator = $this->getChildOfType(0, 'n_OPERATOR'); $operand = $this->getChildByIndex(1); switch ($operator->getSemanticString()) { case '-': return -$operand->evalStatic(); break; case '+': return $operand->evalStatic(); break; default: throw new Exception("Unexpected operator in static expression."); } break; case 'n_ARRAY_LITERAL': $result = array(); $values = $this->getChildOfType(0, 'n_ARRAY_VALUE_LIST'); foreach ($values->getChildren() as $child) { $key = $child->getChildByIndex(0); $val = $child->getChildByIndex(1); if ($key->getTypeName() == 'n_EMPTY') { $result[] = $val->evalStatic(); } else { $result[$key->evalStatic()] = $val->evalStatic(); } } return $result; default: throw new Exception("Unexpected node."); } } public function getStringLiteralValue() { if ($this->getTypeName() != 'n_STRING_SCALAR') { return null; } $value = $this->getSemanticString(); $type = $value[0]; $value = substr($value, 1, -1); $esc = false; $len = strlen($value); $out = ''; if ($type == "'") { // Single quoted strings treat everything as a literal except "\\" and // "\'". return str_replace( array('\\\\', '\\\''), array('\\', "'"), $value); } // Double quoted strings treat "\X" as a literal if X isn't specifically // a character which needs to be escaped -- e.g., "\q" and "\'" are // literally "\q" and "\'". stripcslashes() is too aggressive, so find // all these under-escaped backslashes and escape them. for ($ii = 0; $ii < $len; $ii++) { $c = $value[$ii]; if ($esc) { $esc = false; switch ($c) { case 'x': $u = isset($value[$ii + 1]) ? $value[$ii + 1] : null; if (!preg_match('/^[a-z0-9]/i', $u)) { // PHP treats \x followed by anything which is not a hex digit // as a literal \x. $out .= '\\\\'.$c; break; } case 'n': case 'r': case 'f': case 'v': case '"': case '$': case 't': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': $out .= '\\'.$c; break; default: $out .= '\\\\'.$c; break; } } else if ($c == '\\') { $esc = true; } else { $out .= $c; } } return stripcslashes($out); } public function getLineNumber() { return idx($this->tree->getOffsetToLineNumberMap(), $this->getOffset()); } } diff --git a/src/parser/xhpast/api/XHPASTSyntaxErrorException.php b/src/parser/xhpast/api/XHPASTSyntaxErrorException.php index afa585f..7c9528f 100644 --- a/src/parser/xhpast/api/XHPASTSyntaxErrorException.php +++ b/src/parser/xhpast/api/XHPASTSyntaxErrorException.php @@ -1,35 +1,19 @@ errorLine = $line; parent::__construct($message); } public function getErrorLine() { return $this->errorLine; } } diff --git a/src/parser/xhpast/api/XHPASTToken.php b/src/parser/xhpast/api/XHPASTToken.php index dbfa5e7..0d5b926 100644 --- a/src/parser/xhpast/api/XHPASTToken.php +++ b/src/parser/xhpast/api/XHPASTToken.php @@ -1,58 +1,42 @@ typeName)) { $type_id = $this->typeID; if ($type_id <= 255) { $this->typeName = chr($type_id); } $this->typeName = parent::getTypeName(); } return $this->typeName; } public function isComment() { static $type_ids = null; if ($type_ids === null) { $type_ids = array( $this->tree->getTokenTypeIDFromTypeName('T_COMMENT') => true, $this->tree->getTokenTypeIDFromTypeName('T_DOC_COMMENT') => true, ); } return isset($type_ids[$this->typeID]); } public function isAnyWhitespace() { static $type_ids = null; if ($type_ids === null) { $type_ids = array( $this->tree->getTokenTypeIDFromTypeName('T_WHITESPACE') => true, ); } return isset($type_ids[$this->typeID]); } } diff --git a/src/parser/xhpast/api/XHPASTTree.php b/src/parser/xhpast/api/XHPASTTree.php index 2c62ba2..3d23048 100644 --- a/src/parser/xhpast/api/XHPASTTree.php +++ b/src/parser/xhpast/api/XHPASTTree.php @@ -1,92 +1,76 @@ setTreeType('XHP'); $this->setNodeConstants(xhp_parser_node_constants()); $this->setTokenConstants(xhpast_parser_token_constants()); parent::__construct($tree, $stream, $source); } public function newNode($id, array $data, AASTTree $tree) { return new XHPASTNode($id, $data, $tree); } public function newToken( $id, $type, $value, $offset, AASTTree $tree) { return new XHPASTToken($id, $type, $value, $offset, $tree); } public static function newFromData($php_source) { $future = xhpast_get_parser_future($php_source); return self::newFromDataAndResolvedExecFuture( $php_source, $future->resolve()); } public static function evalStaticString($string) { $string = 'getRootNode()->selectDescendantsOfType('n_STATEMENT'); if (count($statements) != 1) { throw new Exception("String does not parse into exactly one statement!"); } // Return the first one, trying to use reset() with iterators ends in tears. foreach ($statements as $statement) { return $statement->evalStatic(); } } public static function newFromDataAndResolvedExecFuture( $php_source, array $resolved) { list($err, $stdout, $stderr) = $resolved; if ($err) { if ($err == 1) { $matches = null; $is_syntax = preg_match( '/^XHPAST Parse Error: (.*) on line (\d+)/', $stderr, $matches); if ($is_syntax) { throw new XHPASTSyntaxErrorException($matches[2], $stderr); } } throw new Exception("XHPAST failed to parse file data {$err}: {$stderr}"); } $data = json_decode($stdout, true); if (!is_array($data)) { throw new Exception("XHPAST: failed to decode tree."); } return new XHPASTTree($data['tree'], $data['stream'], $php_source); } } diff --git a/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php b/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php index 8964d49..04189ca 100644 --- a/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php +++ b/src/parser/xhpast/api/__tests__/XHPASTTreeTestCase.php @@ -1,149 +1,133 @@ assertEval(1, '1'); $this->assertEval("a", '"a"'); $this->assertEval(-1.1, '-1.1'); $this->assertEval( array('foo', 'bar', -1, +2, -3.4, +4.3, 1e10, 1e-5, -2.3e7), "array('foo', 'bar', -1, +2, -3.4, +4.3, 1e10, 1e-5, -2.3e7)"); $this->assertEval( array(), "array()"); $this->assertEval( array(42 => 7, 'a' => 5, 1, 2, 3, 4, 1 => 'goo'), "array(42 => 7, 'a' => 5, 1, 2, 3, 4, 1 => 'goo')"); $this->assertEval( array('a' => 'a', 'b' => array(1, 2, array(3))), "array('a' => 'a', 'b' => array(1, 2, array(3)))"); $this->assertEval( array(true, false, null), "array(true, false, null)"); // Duplicate keys $this->assertEval( array(0 => '1', 0 => '2'), "array(0 => '1', 0 => '2')"); $this->assertEval('simple string', "'simple string'"); $this->assertEval('42', "'42'"); $this->assertEval(3.1415926, "3.1415926"); $this->assertEval(42, '42'); $this->assertEval( array(2147483648, 2147483647, -2147483648, -2147483647), "array(2147483648, 2147483647, -2147483648, -2147483647)"); $this->assertEval(INF, 'INF'); $this->assertEval(-INF, '-INF'); $this->assertEval(0x1b, '0x1b'); $this->assertEval(0X0A, '0X0A'); // Octal $this->assertEval(010, '010'); $this->assertEval(080, '080'); // Invalid! // Leading 0, but float, not octal. $this->assertEval(0.11e1, '0.11e1'); $this->assertEval(0e1, '0e1'); $this->assertEval(0, '0'); // Static evaluation treats '$' as a literal dollar glyph. $this->assertEval('$asdf', '"$asdf"'); $this->assertEval( '\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z'. '\1\2\3\4\5\6\7\8\9\0'. '\!\@\#\$\%\^\&\*\(\)'. '\`\~\\\|\[\]\{\}\<\>\,\.\/\?\:\;\-\_\=\+', "'\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q". "\\r\\s\\t\\u\\v\\w\\x\\y\\z". "\\1\\2\\3\\4\\5\\6\\7\\8\\9\\0". "\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)". "\\`\\~\\\\\\|\\[\\]\\{\\}\\<\\>\\,\\.\\/\\?\\:\\;\\-\\_\\=\\+". "'"); $this->assertEval( "\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z". "\1\2\3\4\5\6\7\8\9\0". "\!\@\#\$\%\^\&\*\(\)". "\`\~\\\|\[\]\{\}\<\>\,\.\/\?\:\;\-\_\=\+", '"\\a\\b\\c\\d\\e\\f\\g\\h\\i\\j\\k\\l\\m\\n\\o\\p\\q'. '\\r\\s\\t\\u\\v\\w\\x\\y\\z'. '\\1\\2\\3\\4\\5\\6\\7\\8\\9\\0'. '\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)'. '\\`\\~\\\\\\|\\[\\]\\{\\}\\<\\>\\,\\.\\/\\?\\:\\;\\-\\_\\=\\+"'); $this->assertEval( '\' "', "'\\' \"'"); $this->assertEval( '\\ \\\\ ', '\'\\\\ \\\\\\\\ \''); $this->assertEval( '\ \\ ', "'\\ \\\\ '"); $this->assertEval( '\x92', '\'\x92\''); $this->assertEval( "\x92", '"\x92"'); $this->assertEval( "\x", '"\x"'); $this->assertEval( "\x1", '"\x1"'); $this->assertEval( "\x000 !", '"\x000 !"'); $this->assertEval( "\x0", '"\x0"'); } private function assertEval($value, $string) { $this->assertEqual( $value, XHPASTTree::evalStaticString($string), $string); } } diff --git a/src/parser/xhpast/bin/xhpast_parse.php b/src/parser/xhpast/bin/xhpast_parse.php index 638032b..d566f26 100644 --- a/src/parser/xhpast/bin/xhpast_parse.php +++ b/src/parser/xhpast/bin/xhpast_parse.php @@ -1,79 +1,63 @@ write($data); return $future; } diff --git a/src/parser/xhpast/parser_tokens.php b/src/parser/xhpast/parser_tokens.php index 9c31c41..437f262 100644 --- a/src/parser/xhpast/parser_tokens.php +++ b/src/parser/xhpast/parser_tokens.php @@ -1,154 +1,138 @@ 'T_REQUIRE_ONCE', 259 => 'T_REQUIRE', 260 => 'T_EVAL', 261 => 'T_INCLUDE_ONCE', 262 => 'T_INCLUDE', 263 => 'T_LOGICAL_OR', 264 => 'T_LOGICAL_XOR', 265 => 'T_LOGICAL_AND', 266 => 'T_PRINT', 267 => 'T_SR_EQUAL', 268 => 'T_SL_EQUAL', 269 => 'T_XOR_EQUAL', 270 => 'T_OR_EQUAL', 271 => 'T_AND_EQUAL', 272 => 'T_MOD_EQUAL', 273 => 'T_CONCAT_EQUAL', 274 => 'T_DIV_EQUAL', 275 => 'T_MUL_EQUAL', 276 => 'T_MINUS_EQUAL', 277 => 'T_PLUS_EQUAL', 278 => 'T_BOOLEAN_OR', 279 => 'T_BOOLEAN_AND', 280 => 'T_IS_NOT_IDENTICAL', 281 => 'T_IS_IDENTICAL', 282 => 'T_IS_NOT_EQUAL', 283 => 'T_IS_EQUAL', 284 => 'T_IS_GREATER_OR_EQUAL', 285 => 'T_IS_SMALLER_OR_EQUAL', 286 => 'T_SR', 287 => 'T_SL', 288 => 'T_INSTANCEOF', 289 => 'T_UNSET_CAST', 290 => 'T_BOOL_CAST', 291 => 'T_OBJECT_CAST', 292 => 'T_ARRAY_CAST', 293 => 'T_BINARY_CAST', 294 => 'T_UNICODE_CAST', 295 => 'T_STRING_CAST', 296 => 'T_DOUBLE_CAST', 297 => 'T_INT_CAST', 298 => 'T_DEC', 299 => 'T_INC', 300 => 'T_CLONE', 301 => 'T_NEW', 302 => 'T_EXIT', 303 => 'T_IF', 304 => 'T_ELSEIF', 305 => 'T_ELSE', 306 => 'T_ENDIF', 307 => 'T_LNUMBER', 308 => 'T_DNUMBER', 309 => 'T_STRING', 310 => 'T_STRING_VARNAME', 311 => 'T_VARIABLE', 312 => 'T_NUM_STRING', 313 => 'T_INLINE_HTML', 314 => 'T_CHARACTER', 315 => 'T_BAD_CHARACTER', 316 => 'T_ENCAPSED_AND_WHITESPACE', 317 => 'T_CONSTANT_ENCAPSED_STRING', 318 => 'T_BACKTICKS_EXPR', 319 => 'T_ECHO', 320 => 'T_DO', 321 => 'T_WHILE', 322 => 'T_ENDWHILE', 323 => 'T_FOR', 324 => 'T_ENDFOR', 325 => 'T_FOREACH', 326 => 'T_ENDFOREACH', 327 => 'T_DECLARE', 328 => 'T_ENDDECLARE', 329 => 'T_AS', 330 => 'T_SWITCH', 331 => 'T_ENDSWITCH', 332 => 'T_CASE', 333 => 'T_DEFAULT', 334 => 'T_BREAK', 335 => 'T_CONTINUE', 336 => 'T_GOTO', 337 => 'T_FUNCTION', 338 => 'T_CONST', 339 => 'T_RETURN', 340 => 'T_TRY', 341 => 'T_CATCH', 342 => 'T_THROW', 343 => 'T_USE', 344 => 'T_GLOBAL', 345 => 'T_PUBLIC', 346 => 'T_PROTECTED', 347 => 'T_PRIVATE', 348 => 'T_FINAL', 349 => 'T_ABSTRACT', 350 => 'T_STATIC', 351 => 'T_VAR', 352 => 'T_UNSET', 353 => 'T_ISSET', 354 => 'T_EMPTY', 355 => 'T_HALT_COMPILER', 356 => 'T_CLASS', 357 => 'T_INTERFACE', 358 => 'T_EXTENDS', 359 => 'T_IMPLEMENTS', 360 => 'T_OBJECT_OPERATOR', 361 => 'T_DOUBLE_ARROW', 362 => 'T_LIST', 363 => 'T_ARRAY', 364 => 'T_CLASS_C', 365 => 'T_METHOD_C', 366 => 'T_FUNC_C', 367 => 'T_LINE', 368 => 'T_FILE', 369 => 'T_COMMENT', 370 => 'T_DOC_COMMENT', 371 => 'T_OPEN_TAG', 372 => 'T_OPEN_TAG_WITH_ECHO', 373 => 'T_OPEN_TAG_FAKE', 374 => 'T_CLOSE_TAG', 375 => 'T_WHITESPACE', 376 => 'T_START_HEREDOC', 377 => 'T_END_HEREDOC', 378 => 'T_HEREDOC', 379 => 'T_DOLLAR_OPEN_CURLY_BRACES', 380 => 'T_CURLY_OPEN', 381 => 'T_PAAMAYIM_NEKUDOTAYIM', 382 => 'T_BINARY_DOUBLE', 383 => 'T_BINARY_HEREDOC', 384 => 'T_NAMESPACE', 385 => 'T_NS_C', 386 => 'T_DIR', 387 => 'T_NS_SEPARATOR', ); } diff --git a/src/readableserializer/PhutilReadableSerializer.php b/src/readableserializer/PhutilReadableSerializer.php index 2d1ce84..cb81959 100644 --- a/src/readableserializer/PhutilReadableSerializer.php +++ b/src/readableserializer/PhutilReadableSerializer.php @@ -1,189 +1,173 @@ 1) { $str .= 'of size '.count($value).' starting with: '; } reset($value); // Prevent key() from giving warning message in HPHP. $str .= '{ '.key($value).' => '. self::printShort(head($value)).' }'; } return $str; } else { return self::printableValue($value); } } /** * Dump some debug output about an object's members without the * potential recursive explosion of verbosity that comes with ##print_r()##. * * To print any number of member variables, pass null for $max_members. * * @param wild Any value. * @param int Maximum depth to print for nested arrays and objects. * @param int Maximum number of values to print at each level. * @return string Human-readable shallow representation of the value. * @task print */ public static function printShallow( $value, $max_depth = 2, $max_members = 25) { return self::printShallowRecursive($value, $max_depth, $max_members, 0, ''); } /* -( Internals )---------------------------------------------------------- */ /** * Implementation for @{method:printShallow}. * * @param wild Any value. * @param int Maximum depth to print for nested arrays and objects. * @param int Maximum number of values to print at each level. * @param int Current depth. * @param string Indentation string. * @return string Human-readable shallow representation of the value. * @task internal */ private static function printShallowRecursive( $value, $max_depth, $max_members, $depth, $indent) { if (!is_object($value) && !is_array($value)) { return self::addIndentation(self::printableValue($value), $indent, 1); } $ret = ''; if (is_object($value)) { $ret = get_class($value)."\nwith members "; $value = array_filter(@(array)$value); // Remove null characters that magically appear around keys for // member variables of parent classes. $transformed = array(); foreach ($value as $key => $x) { $transformed[str_replace("\0", ' ', $key)] = $x; } $value = $transformed; } if ($max_members !== null) { $value = array_slice($value, 0, $max_members, $preserve_keys = true); } $shallow = array(); if ($depth < $max_depth) { foreach ($value as $k => $v) { $shallow[$k] = self::printShallowRecursive( $v, $max_depth, $max_members, $depth + 1, ' '); } } else { foreach ($value as $k => $v) { // Extra indentation is for empty arrays, because they wrap on multiple // lines and lookup stupid without the extra indentation $shallow[$k] = self::addIndentation(self::printShort($v), $indent, 1); } } return self::addIndentation($ret.print_r($shallow, true), $indent, 1); } /** * Adds indentation to the beginning of every line starting from $first_line. * * @param string Printed value. * @param string String to indent with. * @param int Index of first line to indent. * @return string * @task internal */ private static function addIndentation($value, $indent, $first_line) { $lines = explode("\n", $value); $out = array(); foreach ($lines as $index => $line) { $out[] = $index >= $first_line ? $indent.$line : $line; } return implode("\n", $out); } } diff --git a/src/readableserializer/__tests__/PhutilReadableSerializerTestCase.php b/src/readableserializer/__tests__/PhutilReadableSerializerTestCase.php index d825906..529c8c6 100644 --- a/src/readableserializer/__tests__/PhutilReadableSerializerTestCase.php +++ b/src/readableserializer/__tests__/PhutilReadableSerializerTestCase.php @@ -1,39 +1,23 @@ assertEqual( $expect, PhutilReadableSerializer::printableValue($value)); } } } diff --git a/src/serviceprofiler/PhutilServiceProfiler.php b/src/serviceprofiler/PhutilServiceProfiler.php index 03db9df..4560e07 100644 --- a/src/serviceprofiler/PhutilServiceProfiler.php +++ b/src/serviceprofiler/PhutilServiceProfiler.php @@ -1,135 +1,119 @@ discardMode = true; } public static function getInstance() { if (empty(self::$instance)) { self::$instance = new PhutilServiceProfiler(); } return self::$instance; } public function beginServiceCall(array $data) { $data['begin'] = microtime(true); $id = $this->logSize++; $this->events[$id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'begin', $id, $data); } return $id; } public function endServiceCall($call_id, array $data) { $data = ($this->events[$call_id] + $data); $data['end'] = microtime(true); $data['duration'] = ($data['end'] - $data['begin']); $this->events[$call_id] = $data; foreach ($this->listeners as $listener) { call_user_func($listener, 'end', $call_id, $data); } if ($this->discardMode) { unset($this->events[$call_id]); } } public function getServiceCallLog() { return $this->events; } public function addListener($callback) { $this->listeners[] = $callback; } public static function installEchoListener() { $instance = PhutilServiceProfiler::getInstance(); $instance->addListener(array('PhutilServiceProfiler', 'echoListener')); } public static function echoListener($type, $id, $data) { $is_begin = false; $is_end = false; switch ($type) { case 'begin': $is_begin = true; $mark = '>>>'; break; case 'end': $is_end = true; $mark = '<<<'; break; default: $mark = null; break; } $type = idx($data, 'type', 'mystery'); $desc = null; if ($is_begin) { switch ($type) { case 'query': $desc = substr($data['query'], 0, 512); break; case 'exec': $desc = '$ '.$data['command']; break; case 'conduit': $desc = $data['method'].'()'; break; case 'lock': $desc = $data['name']; break; case 'event': $desc = $data['kind'].' '; break; } } else if ($is_end) { $desc = number_format((int)(1000000 * $data['duration'])).' us'; } $console = PhutilConsole::getConsole(); $console->writeLog( "%s [%s] <%s> %s\n", $mark, $id, $type, $desc); } } diff --git a/src/sprites/PhutilSprite.php b/src/sprites/PhutilSprite.php index c640da1..c8930e3 100644 --- a/src/sprites/PhutilSprite.php +++ b/src/sprites/PhutilSprite.php @@ -1,78 +1,62 @@ targetCSS = $target_css; return $this; } public function getTargetCSS() { return $this->targetCSS; } public function setSourcePosition($x, $y) { $this->sourceX = $x; $this->sourceY = $y; return $this; } public function setSourceSize($w, $h) { $this->sourceW = $w; $this->sourceH = $h; return $this; } public function getSourceH() { return $this->sourceH; } public function getSourceW() { return $this->sourceW; } public function getSourceY() { return $this->sourceY; } public function getSourceX() { return $this->sourceX; } public function setSourceFile($source_file) { $this->sourceFile = $source_file; return $this; } public function getSourceFile() { return $this->sourceFile; } } diff --git a/src/sprites/PhutilSpriteSheet.php b/src/sprites/PhutilSpriteSheet.php index feac79a..d35c24c 100644 --- a/src/sprites/PhutilSpriteSheet.php +++ b/src/sprites/PhutilSpriteSheet.php @@ -1,118 +1,102 @@ generated = false; $this->sprites[] = $sprite; return $this; } public function setCSSHeader($header) { $this->generated = false; $this->cssHeader = $header; return $this; } private function generate() { if ($this->generated) { return; } $css = array(); if ($this->cssHeader) { $css[] = $this->cssHeader; } $vertical_margin = 1; $out_w = 0; $out_h = 0; foreach ($this->sprites as $sprite) { $out_w = max($out_w, $sprite->getSourceW()); $out_h += $sprite->getSourceH(); $out_h += $vertical_margin; } $out = imagecreatetruecolor($out_w, $out_h); imagesavealpha($out, true); imagefill($out, 0, 0, imagecolorallocatealpha($out, 0, 0, 0, 127)); $pos_x = 0; $pos_y = 0; foreach ($this->sprites as $sprite) { $src = $this->loadSource($sprite); imagecopy( $out, $src, $pos_x, $pos_y, $sprite->getSourceX(), $sprite->getSourceY(), $sprite->getSourceW(), $sprite->getSourceH()); $rule = $sprite->getTargetCSS(); $cssx = (-$pos_x).'px'; $cssy = (-$pos_y).'px'; $css[] = "{$rule} {\n background-position: {$cssx} {$cssy};\n}"; $pos_y += $sprite->getSourceH(); $pos_y += $vertical_margin; } $this->image = $out; $this->css = implode("\n\n", $css)."\n"; $this->generated = true; } public function generateImage($path) { $this->generate(); $this->log("Writing sprite '{$path}'..."); imagepng($this->image, $path); } public function generateCSS($path) { $this->generate(); $this->log("Writing CSS '{$path}'..."); Filesystem::writeFile($path, $this->css); } private function log($message) { echo $message."\n"; } private function loadSource(PhutilSprite $sprite) { $file = $sprite->getSourceFile(); if (empty($this->sources[$file])) { $data = Filesystem::readFile($file); $this->sources[$file] = imagecreatefromstring($data); } return $this->sources[$file]; } } diff --git a/src/symbols/PhutilSymbolLoader.php b/src/symbols/PhutilSymbolLoader.php index f6a8604..683cdb8 100644 --- a/src/symbols/PhutilSymbolLoader.php +++ b/src/symbols/PhutilSymbolLoader.php @@ -1,421 +1,405 @@ setType('class') * ->setLibrary('example') * ->selectAndLoadSymbols(); * * When you execute the loading query, it returns a dictionary of matching * symbols: * * array( * 'class$Example' => array( * 'type' => 'class', * 'name' => 'Example', * 'library' => 'libexample', * 'module' => 'examples/example', // Deprecated. * 'where' => 'examples/example.php', * ), * // ... more ... * ); * * The **library** and **where** keys show where the symbol is defined. The * **type** and **name** keys identify the symbol itself. * * TODO: Modules will not be supported soon, as they are dropped from * libphutil v2. * * NOTE: This class must not use libphutil funtions, including id() and idx(). * * @task config Configuring the Query * @task load Loading Symbols * @task internal Internals * * @group library */ final class PhutilSymbolLoader { private $type; private $library; private $base; private $module; private $name; private $concrete; private $pathPrefix; private $suppressLoad; /** * Select the type of symbol to load, either ##class## or ##function##. * * @param string Type of symbol to load. * @return this * @task config */ public function setType($type) { $this->type = $type; return $this; } /** * Restrict the symbol query to a specific library; only symbols from this * library will be loaded. * * @param string Library name. * @return this * @task config */ public function setLibrary($library) { // Validate the library name; this throws if the library in not loaded. $bootloader = PhutilBootloader::getInstance(); $bootloader->getLibraryRoot($library); $this->library = $library; return $this; } /** * Restrict the symbol query to a specific path prefix; only symbols defined * in files below that path will be selected. * * @param string Path relative to library root, like "apps/cheese/". * @return this * @task config */ public function setPathPrefix($path) { $this->pathPrefix = str_replace(DIRECTORY_SEPARATOR, '/', $path); return $this; } /** * Restrict the symbol query to a single module. Deprecated. * * @deprecated * @param string Module name. * @return this * @task config */ public function setModule($module) { // TODO: Remove when we drop v1 support. $this->module = $module; return $this; } /** * Restrict the symbol query to a single symbol name, e.g. a specific class * or function name. * * @param string Symbol name. * @return this * @task config */ public function setName($name) { $this->name = $name; return $this; } /** * Restrict the symbol query to only descendants of some class. This will * strictly select descendants, the base class will not be selected. This * implies loading only classes. * * @param string Base class name. * @return this * @task config */ public function setAncestorClass($base) { $this->base = $base; return $this; } /** * Restrict the symbol query to only concrete symbols; this will filter out * abstract classes. * * NOTE: This currently causes class symbols to load, even if you run * @{method:selectSymbolsWithoutLoading}. * * @param bool True if the query should load only concrete symbols. * @return this * @task config */ public function setConcreteOnly($concrete) { $this->concrete = $concrete; return $this; } /* -( Load )--------------------------------------------------------------- */ /** * Execute the query and select matching symbols, then load the modules where * they are defined so they can be used. * * @return dict A dictionary of matching symbols. See top-level class * documentation for details. These symbols will be loaded * and available. * @task load */ public function selectAndLoadSymbols() { $map = array(); $bootloader = PhutilBootloader::getInstance(); if ($this->library) { $libraries = array($this->library); } else { $libraries = $bootloader->getAllLibraries(); } if ($this->type) { $types = array($this->type); } else { $types = array( 'class', 'function', ); } $symbols = array(); foreach ($libraries as $library) { $map = $bootloader->getLibraryMap($library); foreach ($types as $type) { if ($type == 'interface') { $lookup_map = $map['class']; } else { $lookup_map = $map[$type]; } // As an optimization, we filter the list of candidate symbols in // several passes, applying a name-based filter first if possible since // it is highly selective and guaranteed to match at most one symbol. // This is the common case and we land here through __autoload() so it's // worthwhile to microoptimize a bit because this code path is very hot // and we save 5-10ms per page for a very moderate increase in // complexity. if ($this->name) { // If we have a name filter, just pick the matching name out if it // exists. if (isset($lookup_map[$this->name])) { $filtered_map = array( $this->name => $lookup_map[$this->name], ); } else { $filtered_map = array(); } } else { // Otherwise, start with everything. $filtered_map = $lookup_map; } if ($this->module) { foreach ($filtered_map as $name => $module) { if ($module != $this->module) { unset($filtered_map[$name]); } } } if ($this->pathPrefix) { $len = strlen($this->pathPrefix); foreach ($filtered_map as $name => $where) { if (strncmp($where, $this->pathPrefix, $len) !== 0) { unset($filtered_map[$name]); } } } foreach ($filtered_map as $name => $module) { $symbols[$type.'$'.$name] = array( 'type' => $type, 'name' => $name, 'library' => $library, // libphutil v1 'module' => $module, // libphutil v2 'where' => $module, ); } } } if ($this->base) { $names = $this->selectDescendantsOf( $bootloader->getClassTree(), $this->base); foreach ($symbols as $symbol_key => $symbol) { $type = $symbol['type']; if ($type == 'class' || $type == 'interface') { if (isset($names[$symbol['name']])) { continue; } } unset($symbols[$symbol_key]); } } if (!$this->suppressLoad) { $caught = null; foreach ($symbols as $symbol) { try { $this->loadSymbol($symbol); } catch (Exception $ex) { $caught = $ex; } } if ($caught) { // NOTE: We try to load everything even if we fail to load something, // primarily to make it possible to remove functions from a libphutil // library without breaking library startup. throw $caught; } } if ($this->concrete) { // Remove 'abstract' classes. foreach ($symbols as $key => $symbol) { if ($symbol['type'] == 'class') { $reflection = new ReflectionClass($symbol['name']); if ($reflection->isAbstract()) { unset($symbols[$key]); } } } } return $symbols; } /** * Execute the query and select matching symbols, but do not load the modules * where they are defined. This will perform slightly better if you are only * interested in the existence of the symbols and don't plan to use them; * otherwise, use ##selectAndLoadSymbols()##. * * @return dict A dictionary of matching symbols. See top-level class * documentation for details. * @task load */ public function selectSymbolsWithoutLoading() { $this->suppressLoad = true; $result = $this->selectAndLoadSymbols(); $this->suppressLoad = false; return $result; } public static function loadClass($class_name) { // TODO: Remove this method once we drop libphutil v1 support. } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function selectDescendantsOf(array $tree, $root) { $result = array(); if (empty($tree[$root])) { // No known descendants. return array(); } foreach ($tree[$root] as $child) { $result[$child] = true; if (!empty($tree[$child])) { $result += $this->selectDescendantsOf($tree, $child); } } return $result; } /** * @task internal */ private function loadSymbol(array $symbol_spec) { // Check if we've already loaded the symbol; bail if we have. $name = $symbol_spec['name']; $is_function = ($symbol_spec['type'] == 'function'); if ($is_function) { if (function_exists($name)) { return; } } else { if (class_exists($name, false) || interface_exists($name, false)) { return; } } $lib_name = $symbol_spec['library']; $bootloader = PhutilBootloader::getInstance(); $version = $bootloader->getLibraryFormatVersion($lib_name); switch ($version) { case 1: // TODO: Remove this once we drop libphutil v1 support. $bootloader->loadModule( $symbol_spec['library'], $symbol_spec['module']); break; case 2: $bootloader->loadLibrarySource( $symbol_spec['library'], $symbol_spec['where']); break; } // Check that we successfully loaded the symbol from wherever it was // supposed to be defined. if ($is_function) { if (!function_exists($name)) { throw new PhutilMissingSymbolException($name); } } else { if (!class_exists($name, false) && !interface_exists($name, false)) { throw new PhutilMissingSymbolException($name); } } } } diff --git a/src/symbols/exception/PhutilMissingSymbolException.php b/src/symbols/exception/PhutilMissingSymbolException.php index 2c8fcbf..5a5cb6a 100644 --- a/src/symbols/exception/PhutilMissingSymbolException.php +++ b/src/symbols/exception/PhutilMissingSymbolException.php @@ -1,32 +1,16 @@ addNodes( * array( * $object->getPHID() => $object->getChildPHIDs(), * )); * $detector->loadGraph(); * * Now you can query the graph, e.g. by detecting cycles: * * $cycle = $detector->detectCycles($object->getPHID()); * * If ##$cycle## is empty, no graph cycle is reachable from the node. If it * is nonempty, it contains a list of nodes which form a graph cycle. * * NOTE: Nodes must be represented with scalars. * * @task build Graph Construction * @task cycle Cycle Detection * @task explore Graph Exploration * * @group util * @stable */ abstract class AbstractDirectedGraph { private $knownNodes = array(); private $graphLoaded = false; /* -( Graph Construction )------------------------------------------------- */ /** * Load the edges for a list of nodes. You must override this method. You * will be passed a list of nodes, and should return a dictionary mapping * each node to the list of nodes that can be reached by following its the * edges which originate at it: for example, the child nodes of an object * which has a parent-child relationship to other objects. * * The intent of this method is to allow you to issue a single query per * graph level for graphs which are stored as edge tables in the database. * Generally, you will load all the objects which correspond to the list of * nodes, and then return a map from each of their IDs to all their children. * * NOTE: You must return an entry for every node you are passed, even if it * is invalid or can not be loaded. Either return an empty array (if this is * acceptable for your application) or throw an exception if you can't satisfy * this requirement. * * @param list A list of nodes. * @return dict A map of nodes to the nodes reachable along their edges. * There must be an entry for each node you were provided. * @task build */ abstract protected function loadEdges(array $nodes); /** * Seed the graph with known nodes. Often, you will provide the candidate * edges that a user is trying to create here, or the initial set of edges * you know about. * * @param dict A map of nodes to the nodes reachable along their edges. * @return this * @task build */ final public function addNodes(array $nodes) { if ($this->graphLoaded) { throw new Exception( "Call addNodes() before calling loadGraph(). You can not add more ". "nodes once you have loaded the graph."); } $this->knownNodes += $nodes; return $this; } /** * Load the graph, building it out so operations can be performed on it. This * constructs the graph level-by-level, calling @{method:loadEdges} to * expand the graph at each stage until it is complete. * * @return this * @task build */ final public function loadGraph() { $new_nodes = $this->knownNodes; while (true) { $load = array(); foreach ($new_nodes as $node => $edges) { foreach ($edges as $edge) { if (!isset($this->knownNodes[$edge])) { $load[$edge] = true; } } } if (empty($load)) { break; } $load = array_keys($load); $new_nodes = $this->loadEdges($load); foreach ($load as $node) { if (!isset($new_nodes[$node]) || !is_array($new_nodes[$node])) { throw new Exception( "loadEdges() must return an edge list array for each provided ". "node, or the cycle detection algorithm may not terminate."); } } $this->addNodes($new_nodes); } $this->graphLoaded = true; return $this; } /* -( Cycle Detection )---------------------------------------------------- */ /** * Detect if there are any cycles reachable from a given node. * * If cycles are reachable, it returns a list of nodes which create a cycle. * Note that this list may include nodes which aren't actually part of the * cycle, but lie on the graph between the specified node and the cycle. * For example, it might return something like this (when passed "A"): * * A, B, C, D, E, C * * This means you can walk from A to B to C to D to E and then back to C, * which forms a cycle. A and B are included even though they are not part * of the cycle. When presenting information about graph cycles to users, * including these nodes is generally useful. This also shouldn't ever happen * if you've vetted prior edges before writing them, because it means there * is a preexisting cycle in the graph. * * NOTE: This only detects cycles reachable from a node. It does not detect * cycles in the entire graph. * * @param scalar The node to walk from, looking for graph cycles. * @return list|null Returns null if no cycles are reachable from the node, * or a list of nodes that form a cycle. * @task cycle */ final public function detectCycles($node) { if (!$this->graphLoaded) { throw new Exception( "Call loadGraph() to build the graph out before calling ". "detectCycles()."); } if (!isset($this->knownNodes[$node])) { throw new Exception( "The node '{$node}' is not known. Call addNodes() to seed the graph ". "with nodes."); } $visited = array(); return $this->performCycleDetection($node, $visited); } /** * Internal cycle detection implementation. Recursively walks the graph, * keeping track of where it's been, and returns the first cycle it finds. * * @param scalar The node to walk from. * @param list Previously visited nodes. * @return null|list Null if no cycles are found, or a list of nodes * which cycle. * @task cycle */ final private function performCycleDetection($node, array $visited) { $visited[$node] = true; foreach ($this->knownNodes[$node] as $edge) { if (isset($visited[$edge])) { $result = array_keys($visited); $result[] = $edge; return $result; } $result = $this->performCycleDetection($edge, $visited); if ($result) { return $result; } } return null; } } diff --git a/src/utils/PhutilBufferedIterator.php b/src/utils/PhutilBufferedIterator.php index 219807f..c4b07c3 100644 --- a/src/utils/PhutilBufferedIterator.php +++ b/src/utils/PhutilBufferedIterator.php @@ -1,156 +1,140 @@ List of results. * @task impl */ abstract protected function loadPage(); /* -( Configuration )------------------------------------------------------ */ /** * Get the configured page size. * * @return int Page size. * @task config */ final public function getPageSize() { return $this->pageSize; } /** * Configure the page size. Note that implementations may ignore this. * * @param int Page size. * @return this * @task config */ final public function setPageSize($size) { $this->pageSize = $size; return $this; } /* -( Iterator Implementation )-------------------------------------------- */ /** * @task iterator */ final public function rewind() { $this->didRewind(); $this->data = array(); $this->naturalKey = 0; $this->next(); } /** * @task iterator */ final public function valid() { return (bool)$this->data; } /** * @task iterator */ final public function current() { return end($this->data); } /** * By default, the iterator assigns a "natural" key (0, 1, 2, ...) to each * result. This method is intentionally nonfinal so you can substitute a * different behavior by overriding it if you prefer. * * @return scalar Key for the current result (as per @{method:current}). * @task iterator */ public function key() { return $this->naturalKey; } /** * @task iterator */ final public function next() { if ($this->data) { $this->naturalKey++; array_pop($this->data); if ($this->data) { return; } } $data = $this->loadPage(); // NOTE: Reverse the results so we can use array_pop() to discard them, // since it doesn't have the O(N) key reassignment behavior of // array_shift(). $this->data = array_reverse($data); } } diff --git a/src/utils/PhutilBufferedIteratorExample.php b/src/utils/PhutilBufferedIteratorExample.php index 79e3fdc..5e71922 100644 --- a/src/utils/PhutilBufferedIteratorExample.php +++ b/src/utils/PhutilBufferedIteratorExample.php @@ -1,50 +1,34 @@ cursor = 0; } protected function loadPage() { $result = $this->query($this->cursor, $this->getPageSize()); $this->cursor += count($result); return $result; } public function setExampleData(array $data) { $this->data = $data; } private function query($cursor, $limit) { // NOTE: Normally you'd load or generate results from some external source // here. Since this is an example, we just use a premade dataset. return array_slice($this->data, $cursor, $limit); } } diff --git a/src/utils/__tests__/AbstractDirectedGraphTestCase.php b/src/utils/__tests__/AbstractDirectedGraphTestCase.php index ebea9be..521f56a 100644 --- a/src/utils/__tests__/AbstractDirectedGraphTestCase.php +++ b/src/utils/__tests__/AbstractDirectedGraphTestCase.php @@ -1,107 +1,91 @@ array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'Trivial Graph'); } public function testNoncyclicGraph() { $graph = array( 'A' => array('B', 'C'), 'B' => array('D'), 'C' => array(), 'D' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'Noncyclic Graph'); } public function testTrivialCyclicGraph() { $graph = array( 'A' => array('A'), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(array('A', 'A'), $cycle, 'Trivial Cycle'); } public function testCyclicGraph() { $graph = array( 'A' => array('B', 'C'), 'B' => array('D'), 'C' => array('E', 'F'), 'D' => array(), 'E' => array(), 'F' => array('G', 'C'), 'G' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(array('A', 'C', 'F', 'C'), $cycle, 'Cyclic Graph'); } public function testNonTreeGraph() { // This graph is non-cyclic, but C is both a child and a grandchild of A. // This is permitted. $graph = array( 'A' => array('B', 'C'), 'B' => array('C'), 'C' => array(), ); $cycle = $this->findGraphCycle($graph); $this->assertEqual(null, $cycle, 'NonTreeGraph'); } public function testEdgeLoadFailure() { $graph = array( 'A' => array('B'), ); $raised = null; try { $this->findGraphCycle($graph); } catch (Exception $ex) { $raised = $ex; } $this->assertEqual( true, (bool)$raised, 'Exception raised by unloadable edges.'); } private function findGraphCycle(array $graph, $seed = 'A', $search = 'A') { $detector = new TestAbstractDirectedGraph(); $detector->setTestData($graph); $detector->addNodes(array_select_keys($graph, array($seed))); $detector->loadGraph(); return $detector->detectCycles($search); } } diff --git a/src/utils/__tests__/MFilterTestHelper.php b/src/utils/__tests__/MFilterTestHelper.php index 5ef0d9e..2668002 100644 --- a/src/utils/__tests__/MFilterTestHelper.php +++ b/src/utils/__tests__/MFilterTestHelper.php @@ -1,46 +1,30 @@ h = $h_value; $this->i = $i_value; $this->j = $j_value; } public function getH() { return $this->h; } public function getI() { return $this->i; } public function getJ() { return $this->j; } } diff --git a/src/utils/__tests__/PhutilBufferedIteratorTestCase.php b/src/utils/__tests__/PhutilBufferedIteratorTestCase.php index 2a5d1cd..f8f1f33 100644 --- a/src/utils/__tests__/PhutilBufferedIteratorTestCase.php +++ b/src/utils/__tests__/PhutilBufferedIteratorTestCase.php @@ -1,43 +1,27 @@ setPageSize(3); $iterator->setExampleData($expect); $results = array(); foreach ($iterator as $key => $value) { $results[$key] = $value; } $this->assertEqual( $expect, $results); } } diff --git a/src/utils/__tests__/PhutilUTF8TestCase.php b/src/utils/__tests__/PhutilUTF8TestCase.php index 1025194..aa449ed 100644 --- a/src/utils/__tests__/PhutilUTF8TestCase.php +++ b/src/utils/__tests__/PhutilUTF8TestCase.php @@ -1,273 +1,257 @@ assertEqual($input, phutil_utf8ize($input)); } public function testUTF8ize_UTF8_ignored() { $input = "\xc3\x9c \xc3\xbc \xe6\x9d\xb1!"; $this->assertEqual($input, phutil_utf8ize($input)); } public function testUTF8ize_LongString_nosegfault() { // For some reason my laptop is segfaulting on long inputs inside // preg_match(). Forestall this craziness in the common case, at least. phutil_utf8ize(str_repeat('x', 1024 * 1024)); $this->assertEqual(true, true); } public function testUTF8ize_invalidUTF8_fixed() { $input = "\xc3 this has \xe6\x9d some invalid utf8 \xe6"; $expect = "\xEF\xBF\xBD this has \xEF\xBF\xBD\xEF\xBF\xBD some invalid utf8 ". "\xEF\xBF\xBD"; $result = phutil_utf8ize($input); $this->assertEqual($expect, $result); } public function testUTF8ize_owl_isCuteAndFerocious() { // This was once a ferocious owl when we used to use "?" as the replacement // character instead of U+FFFD, but now he is sort of not as cute or // ferocious. $input = "M(o\xEE\xFF\xFFo)M"; $expect = "M(o\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBDo)M"; $result = phutil_utf8ize($input); $this->assertEqual($expect, $result); } public function testUTF8len() { $strings = array( '' => 0, 'x' => 1, "\xEF\xBF\xBD" => 1, "x\xe6\x9d\xb1y" => 3, "xyz" => 3, 'quack' => 5, ); foreach ($strings as $str => $expect) { $this->assertEqual($expect, phutil_utf8_strlen($str), 'Length of '.$str); } } public function testUTF8v() { $strings = array( '' => array(), 'x' => array('x'), 'quack' => array('q', 'u', 'a', 'c', 'k'), "x\xe6\x9d\xb1y" => array('x', "\xe6\x9d\xb1", 'y'), // This is a combining character. "x\xCD\xA0y" => array("x", "\xCD\xA0", 'y'), ); foreach ($strings as $str => $expect) { $this->assertEqual($expect, phutil_utf8v($str), 'Vector of '.$str); } } public function testUTF8vCodepoints() { $strings = array( '' => array(), 'x' => array(0x78), 'quack' => array(0x71, 0x75, 0x61, 0x63, 0x6B), "x\xe6\x9d\xb1y" => array(0x78, 0x6771, 0x79), "\xC2\xBB" => array(0x00BB), "\xE2\x98\x83" => array(0x2603), "\xEF\xBF\xBF" => array(0xFFFF), "\xF0\x9F\x92\xA9" => array(0x1F4A9), // This is a combining character. "x\xCD\xA0y" => array(0x78, 0x0360, 0x79), ); foreach ($strings as $str => $expect) { $this->assertEqual( $expect, phutil_utf8v_codepoints($str), 'Codepoint Vector of '.$str); } } public function testUTF8ConsoleStrlen() { $strings = array( "" => 0, "\0" => 0, "x" => 1, // Double-width chinese character. "\xe6\x9d\xb1" => 2, ); foreach ($strings as $str => $expect) { $this->assertEqual( $expect, phutil_utf8_console_strlen($str), 'Console Length of '.$str); } } public function testUTF8shorten() { $inputs = array( array("1erp derp derp", 9, "", "1erp derp"), array("2erp derp derp", 12, "...", "2erp derp..."), array("derpxderpxderp", 12, "...", "derpxderp..."), array("derp\xE2\x99\x83derpderp", 12, "...", "derp\xE2\x99\x83derp..."), array("", 12, "...", ""), array("derp", 12, "...", "derp"), array("11111", 5, "2222", "11111"), array("111111", 5, "2222", "12222"), array("D1rp. Derp derp.", 7, "...", "D1rp."), array("D2rp. Derp derp.", 5, "...", "D2rp."), array("D3rp. Derp derp.", 4, "...", "D..."), array("D4rp. Derp derp.", 14, "...", "D4rp. Derp..."), array("D5rpderp, derp derp", 16, "...", "D5rpderp..."), array("D6rpderp, derp derp", 17, "...", "D6rpderp, derp..."), // This behavior is maybe a little bad, but it seems mostly reasonable, // at least for latin languages. array("Derp, supercalafragalisticexpialadoshus", 30, "...", "Derp..."), // If a string has only word-break characters in it, we should just cut // it, not produce only the terminal. array("((((((((((", 8, '...', '(((((...'), ); foreach ($inputs as $input) { list($string, $length, $terminal, $expect) = $input; $result = phutil_utf8_shorten($string, $length, $terminal); $this->assertEqual($expect, $result, 'Shortening of '.$string); } try { phutil_utf8_shorten('derp', 3, 'quack'); $caught = false; } catch (Exception $ex) { $caught = true; } $this->assertEqual(true, $caught, 'Expect exception for terminal.'); } public function testUTF8Wrap() { $inputs = array( array( 'aaaaaaa', 3, array( 'aaa', 'aaa', 'a', )), array( 'aaaaaaa', 3, array( 'aaa', 'aaa', 'a', )), array( 'aa&aaaa', 3, array( 'aa&', 'aaa', 'a', )), array( "aa\xe6\x9d\xb1aaaa", 3, array( "aa\xe6\x9d\xb1", 'aaa', 'a', )), array( '', 80, array( )), array( 'a', 80, array( 'a', )), ); foreach ($inputs as $input) { list($string, $width, $expect) = $input; $this->assertEqual( $expect, phutil_utf8_hard_wrap_html($string, $width), "Wrapping of '".$string."'"); } } public function testUTF8ConvertParams() { $caught = null; try { phutil_utf8_convert('', 'utf8', ''); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, (bool)$caught, 'Requires source encoding.'); $caught = null; try { phutil_utf8_convert('', '', 'utf8'); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, (bool)$caught, 'Requires target encoding.'); } public function testUTF8Convert() { if (!function_exists('mb_convert_encoding')) { $this->assertSkipped("Requires mbstring extension."); } // "[ae]gis se[n]or [(c)] 1970 [+/-] 1 [degree]" $input = "\xE6gis SE\xD1OR \xA9 1970 \xB11\xB0"; $expect = "\xC3\xA6gis SE\xC3\x91OR \xC2\xA9 1970 \xC2\xB11\xC2\xB0"; $output = phutil_utf8_convert($input, 'UTF-8', 'ISO-8859-1'); $this->assertEqual($expect, $output, 'Conversion from ISO-8859-1.'); $caught = null; try { phutil_utf8_convert('xyz', 'moon language', 'UTF-8'); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, (bool)$caught, 'Conversion with bogus encoding.'); } } diff --git a/src/utils/__tests__/PhutilUtilsTestCase.php b/src/utils/__tests__/PhutilUtilsTestCase.php index e2c4a70..30ef6b2 100644 --- a/src/utils/__tests__/PhutilUtilsTestCase.php +++ b/src/utils/__tests__/PhutilUtilsTestCase.php @@ -1,310 +1,294 @@ assertEqual( true, ($caught instanceof InvalidArgumentException)); } public function testMFilter_withEmptyValue_filtered() { $a = new MFilterTestHelper('o', 'p', 'q'); $b = new MFilterTestHelper('o', '', 'q'); $c = new MFilterTestHelper('o', 'p', 'q'); $list = array( 'a' => $a, 'b' => $b, 'c' => $c, ); $actual = mfilter($list, 'getI'); $expected = array( 'a' => $a, 'c' => $c, ); $this->assertEqual($expected, $actual); } public function testMFilter_withEmptyValueNegate_filtered() { $a = new MFilterTestHelper('o', 'p', 'q'); $b = new MFilterTestHelper('o', '', 'q'); $c = new MFilterTestHelper('o', 'p', 'q'); $list = array( 'a' => $a, 'b' => $b, 'c' => $c, ); $actual = mfilter($list, 'getI', true); $expected = array( 'b' => $b, ); $this->assertEqual($expected, $actual); } public function testIFilter_invalidIndex_throwException() { $caught = null; try { ifilter(array(), null); } catch (InvalidArgumentException $ex) { $caught = $ex; } $this->assertEqual( true, ($caught instanceof InvalidArgumentException)); } public function testIFilter_withEmptyValue_filtered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q',), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q',), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q',), ); $actual = ifilter($list, 'i'); $expected = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), ); $this->assertEqual($expected, $actual); } public function testIFilter_indexNotExists_allFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), ); $actual = ifilter($list, 'NoneExisting'); $expected = array(); $this->assertEqual($expected, $actual); } public function testIFilter_withEmptyValueNegate_filtered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), 'c' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q',), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q',), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q',), ); $actual = ifilter($list, 'i', true); $expected = array( 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), 'd' => array('h' => 'o', 'i' => 0, 'j' => 'q',), 'e' => array('h' => 'o', 'i' => null, 'j' => 'q',), 'f' => array('h' => 'o', 'i' => false, 'j' => 'q',), ); $this->assertEqual($expected, $actual); } public function testIFilter_indexNotExists_notFiltered() { $list = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), ); $actual = ifilter($list, 'NoneExisting', true); $expected = array( 'a' => array('h' => 'o', 'i' => 'p', 'j' => 'q',), 'b' => array('h' => 'o', 'i' => '', 'j' => 'q',), ); $this->assertEqual($expected, $actual); } public function testmergev_merging_basicallyWorksCorrectly() { $this->assertEqual( array(), array_mergev( array( // ))); $this->assertEqual( array(), array_mergev( array( array(), array(), array(), ))); $this->assertEqual( array(1, 2, 3, 4, 5), array_mergev( array( array(1, 2), array(3), array(), array(4, 5), ))); } public function testNonempty() { $this->assertEqual( 'zebra', nonempty(false, null, 0, '', array(), 'zebra')); $this->assertEqual( null, nonempty()); $this->assertEqual( false, nonempty(null, false)); $this->assertEqual( null, nonempty(false, null)); } public function testCoalesce() { $this->assertEqual( 'zebra', coalesce(null, 'zebra')); $this->assertEqual( null, coalesce()); $this->assertEqual( false, coalesce(false, null)); $this->assertEqual( false, coalesce(null, false)); } public function testHeadLast() { $this->assertEqual( 'a', head(explode('.', 'a.b'))); $this->assertEqual( 'b', last(explode('.', 'a.b'))); } public function testHeadKeyLastKey() { $this->assertEqual( 'a', head_key(array('a' => 0, 'b' => 1)) ); $this->assertEqual( 'b', last_key(array('a' => 0, 'b' => 1)) ); $this->assertEqual(NULL, head_key(array())); $this->assertEqual(NULL, last_key(array())); } public function testID() { $this->assertEqual(true, id(true)); $this->assertEqual(false, id(false)); } public function testIdx() { $array = array( 'present' => true, 'null' => null, ); $this->assertEqual(true, idx($array, 'present')); $this->assertEqual(true, idx($array, 'present', false)); $this->assertEqual(null, idx($array, 'null')); $this->assertEqual(null, idx($array, 'null', false)); $this->assertEqual(null, idx($array, 'missing')); $this->assertEqual(false, idx($array, 'missing', false)); } public function testSplitLines() { $retain_cases = array( "" => array(""), "x" => array("x"), "x\n" => array("x\n"), "\n" => array("\n"), "\n\n\n" => array("\n", "\n", "\n"), "\r\n" => array("\r\n"), "x\r\ny\n" => array("x\r\n", "y\n"), "x\ry\nz\r\n" => array("x\ry\n", "z\r\n"), "x\ry\nz\r\n\n" => array("x\ry\n", "z\r\n", "\n"), ); foreach ($retain_cases as $input => $expect) { $this->assertEqual( $expect, phutil_split_lines($input, $retain_endings = true), "(Retained) ".addcslashes($input, "\r\n\\")); } $discard_cases = array( "" => array(""), "x" => array("x"), "x\n" => array("x"), "\n" => array(""), "\n\n\n" => array("", "", ""), "\r\n" => array(""), "x\r\ny\n" => array("x", "y"), "x\ry\nz\r\n" => array("x\ry", "z"), "x\ry\nz\r\n\n" => array("x\ry", "z", ""), ); foreach ($discard_cases as $input => $expect) { $this->assertEqual( $expect, phutil_split_lines($input, $retain_endings = false), "(Discarded) ".addcslashes($input, "\r\n\\")); } } } diff --git a/src/utils/__tests__/TestAbstractDirectedGraph.php b/src/utils/__tests__/TestAbstractDirectedGraph.php index 1006a20..94fbc3a 100644 --- a/src/utils/__tests__/TestAbstractDirectedGraph.php +++ b/src/utils/__tests__/TestAbstractDirectedGraph.php @@ -1,35 +1,19 @@ nodes = $nodes; return $this; } protected function loadEdges(array $nodes) { return array_select_keys($this->nodes, $nodes); } } diff --git a/src/utils/utf8.php b/src/utils/utf8.php index cce9044..f1f0930 100644 --- a/src/utils/utf8.php +++ b/src/utils/utf8.php @@ -1,479 +1,463 @@ = 0x1100 && ($c <= 0x115f || /* Hangul Jamo init. consonants */ $c == 0x2329 || $c == 0x232a || ($c >= 0x2e80 && $c <= 0xa4cf && $c != 0x303f) || /* CJK ... Yi */ ($c >= 0xac00 && $c <= 0xd7a3) || /* Hangul Syllables */ ($c >= 0xf900 && $c <= 0xfaff) || /* CJK Compatibility Ideographs */ ($c >= 0xfe10 && $c <= 0xfe19) || /* Vertical forms */ ($c >= 0xfe30 && $c <= 0xfe6f) || /* CJK Compatibility Forms */ ($c >= 0xff00 && $c <= 0xff60) || /* Fullwidth Forms */ ($c >= 0xffe0 && $c <= 0xffe6) || ($c >= 0x20000 && $c <= 0x2fffd) || ($c >= 0x30000 && $c <= 0x3fffd))); } return $len; } /** * Split a UTF-8 string into an array of characters. Combining characters are * also split. * * @param string A valid utf-8 string. * @return list A list of characters in the string. * @group utf8 */ function phutil_utf8v($string) { $res = array(); $len = strlen($string); $ii = 0; while ($ii < $len) { $byte = $string[$ii]; if ($byte <= "\x7F") { $res[] = $byte; $ii += 1; continue; } else if ($byte < "\xC0") { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } else if ($byte <= "\xDF") { $seq_len = 2; } else if ($byte <= "\xEF") { $seq_len = 3; } else if ($byte <= "\xF7") { $seq_len = 4; } else if ($byte <= "\xFB") { $seq_len = 5; } else if ($byte <= "\xFD") { $seq_len = 6; } else { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } if ($ii + $seq_len > $len) { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } for ($jj = 1; $jj < $seq_len; ++$jj) { if ($string[$ii + $jj] >= "\xC0") { throw new Exception("Invalid UTF-8 string passed to phutil_utf8v()."); } } $res[] = substr($string, $ii, $seq_len); $ii += $seq_len; } return $res; } /** * Split a UTF-8 string into an array of codepoints (as integers). * * @param string A valid UTF-8 string. * @return list A list of codepoints, as integers. * @group utf8 */ function phutil_utf8v_codepoints($string) { $str_v = phutil_utf8v($string); foreach ($str_v as $key => $char) { $c = ord($char[0]); $v = 0; if (($c & 0x80) == 0) { $v = $c; } else if (($c & 0xE0) == 0xC0) { $v = (($c & 0x1F) << 6) + ((ord($char[1]) & 0x3F)); } else if (($c & 0xF0) == 0xE0) { $v = (($c & 0x0F) << 12) + ((ord($char[1]) & 0x3f) << 6) + ((ord($char[2]) & 0x3f)); } else if (($c & 0xF8) == 0xF0) { $v = (($c & 0x07) << 18) + ((ord($char[1]) & 0x3F) << 12) + ((ord($char[2]) & 0x3F) << 6) + ((ord($char[3]) & 0x3f)); } else if (($c & 0xFC) == 0xF8) { $v = (($c & 0x03) << 24) + ((ord($char[1]) & 0x3F) << 18) + ((ord($char[2]) & 0x3F) << 12) + ((ord($char[3]) & 0x3f) << 6) + ((ord($char[4]) & 0x3f)); } else if (($c & 0xFE) == 0xFC) { $v = (($c & 0x01) << 30) + ((ord($char[1]) & 0x3F) << 24) + ((ord($char[2]) & 0x3F) << 18) + ((ord($char[3]) & 0x3f) << 12) + ((ord($char[4]) & 0x3f) << 6) + ((ord($char[5]) & 0x3f)); } $str_v[$key] = $v; } return $str_v; } /** * Shorten a string to provide a summary, respecting UTF-8 characters. This * function attempts to truncate strings at word boundaries. * * NOTE: This function makes a best effort to apply some reasonable rules but * will not work well for the full range of unicode languages. For instance, * no effort is made to deal with combining characters. * * @param string UTF-8 string to shorten. * @param int Maximum length of the result. * @param string If the string is shortened, add this at the end. Defaults to * horizontal ellipsis. * @return string A string with no more than the specified character length. * * @group utf8 */ function phutil_utf8_shorten($string, $length, $terminal = "\xE2\x80\xA6") { $terminal_len = phutil_utf8_strlen($terminal); if ($terminal_len >= $length) { // If you provide a terminal we still enforce that the result (including // the terminal) is no longer than $length, but we can't do that if the // terminal is too long. throw new Exception( "String terminal length must be less than string length!"); } $string_v = phutil_utf8v($string); $string_len = count($string_v); if ($string_len <= $length) { // If the string is already shorter than the requested length, simply return // it unmodified. return $string; } // NOTE: This is not complete, and there are many other word boundary // characters and reasonable places to break words in the UTF-8 character // space. For now, this gives us reasonable behavior for latin langauges. We // don't necessarily have access to PCRE+Unicode so there isn't a great way // for us to look up character attributes. // If we encounter these, prefer to break on them instead of cutting the // string off in the middle of a word. static $break_characters = array( ' ' => true, "\n" => true, ';' => true, ':' => true, '[' => true, '(' => true, ',' => true, '-' => true, ); // If we encounter these, shorten to this character exactly without appending // the terminal. static $stop_characters = array( '.' => true, '!' => true, '?' => true, ); // Search backward in the string, looking for reasonable places to break it. $word_boundary = null; $stop_boundary = null; // If we do a word break with a terminal, we have to look beyond at least the // number of characters in the terminal. $terminal_area = $length - $terminal_len; for ($ii = $length; $ii >= 0; $ii--) { $c = $string_v[$ii]; if (isset($break_characters[$c]) && ($ii <= $terminal_area)) { $word_boundary = $ii; } else if (isset($stop_characters[$c]) && ($ii < $length)) { $stop_boundary = $ii + 1; break; } else { if ($word_boundary !== null) { break; } } } if ($stop_boundary !== null) { // We found a character like ".". Cut the string there, without appending // the terminal. $string_part = array_slice($string_v, 0, $stop_boundary); return implode('', $string_part); } // If we didn't find any boundary characters or we found ONLY boundary // characters, just break at the maximum character length. if ($word_boundary === null || $word_boundary === 0) { $word_boundary = $length - $terminal_len; } $string_part = array_slice($string_v, 0, $word_boundary); $string_part = implode('', $string_part); return $string_part.$terminal; } /** * Hard-wrap a block of UTF-8 text with embedded HTML tags and entities. * * @param string An HTML string with tags and entities. * @return list List of hard-wrapped lines. * @group utf8 */ function phutil_utf8_hard_wrap_html($string, $width) { $break_here = array(); // Convert the UTF-8 string into a list of UTF-8 characters. $vector = phutil_utf8v($string); $len = count($vector); $char_pos = 0; for ($ii = 0; $ii < $len; ++$ii) { // An ampersand indicates an HTML entity; consume the whole thing (until // ";") but treat it all as one character. if ($vector[$ii] == '&') { do { ++$ii; } while ($vector[$ii] != ';'); ++$char_pos; // An "<" indicates an HTML tag, consume the whole thing but don't treat // it as a character. } else if ($vector[$ii] == '<') { do { ++$ii; } while ($vector[$ii] != '>'); } else { ++$char_pos; } // Keep track of where we need to break the string later. if ($char_pos == $width) { $break_here[$ii] = true; $char_pos = 0; } } $result = array(); $string = ''; foreach ($vector as $ii => $char) { $string .= $char; if (isset($break_here[$ii])) { $result[] = $string; $string = ''; } } if (strlen($string)) { $result[] = $string; } return $result; } /** * Convert a string from one encoding (like ISO-8859-1) to another encoding * (like UTF-8). * * This is primarily a thin wrapper around `mb_convert_encoding()` which checks * you have the extension installed, since we try to require the extension * only if you actually need it (i.e., you want to work with encodings other * than UTF-8). * * NOTE: This function assumes that the input is in the given source encoding. * If it is not, it may not output in the specified target encoding. If you * need to perform a hard conversion to UTF-8, use this function in conjunction * with @{function:phutil_utf8ize}. We can detect failures caused by invalid * encoding names, but `mb_convert_encoding()` fails silently if the * encoding name identifies a real encoding but the string is not actually * encoded with that encoding. * * @param string String to re-encode. * @param string Target encoding name, like "UTF-8". * @param string Source endocing name, like "ISO-8859-1". * @return string Input string, with converted character encoding. * * @group utf8 * * @phutil-external-symbol function mb_convert_encoding */ function phutil_utf8_convert($string, $to_encoding, $from_encoding) { if (!$from_encoding) { throw new InvalidArgumentException( "Attempting to convert a string encoding, but no source encoding ". "was provided. Explicitly provide the source encoding."); } if (!$to_encoding) { throw new InvalidArgumentException( "Attempting to convert a string encoding, but no target encoding ". "was provided. Explicitly provide the target encoding."); } // Normalize encoding names so we can no-op the very common case of UTF8 // to UTF8 (or any other conversion where both encodings are identical). $to_upper = strtoupper(str_replace('-', '', $to_encoding)); $from_upper = strtoupper(str_replace('-', '', $from_encoding)); if ($from_upper == $to_upper) { return $string; } if (!function_exists('mb_convert_encoding')) { throw new Exception( "Attempting to convert a string encoding from '{$from_encoding}' ". "to '{$to_encoding}', but the 'mbstring' PHP extension is not ". "available. Install mbstring to work with encodings other than ". "UTF-8."); } $result = @mb_convert_encoding($string, $to_encoding, $from_encoding); if ($result === false) { $message = error_get_last(); if ($message) { $message = idx($message, 'message', 'Unknown error.'); } throw new Exception( "String conversion from encoding '{$from_encoding}' to encoding ". "'{$to_encoding}' failed: {$message}"); } return $result; } diff --git a/src/utils/utils.php b/src/utils/utils.php index 40e831a..7cbc7d5 100644 --- a/src/utils/utils.php +++ b/src/utils/utils.php @@ -1,692 +1,676 @@ doStuff(); * * ...but this works fine: * * id(new Thing())->doStuff(); * * @param wild Anything. * @return wild Unmodified argument. * @group util */ function id($x) { return $x; } /** * Access an array index, retrieving the value stored there if it exists or * a default if it does not. This function allows you to concisely access an * index which may or may not exist without raising a warning. * * @param array Array to access. * @param scalar Index to access in the array. * @param wild Default value to return if the key is not present in the * array. * @return wild If $array[$key] exists, that value is returned. If not, * $default is returned without raising a warning. * @group util */ function idx(array $array, $key, $default = null) { // isset() is a micro-optimization - it is fast but fails for null values. if (isset($array[$key])) { return $array[$key]; } // Comparing $default is also a micro-optimization. if ($default === null || array_key_exists($key, $array)) { return null; } return $default; } /** * Call a method on a list of objects. Short for "method pull", this function * works just like @{function:ipull}, except that it operates on a list of * objects instead of a list of arrays. This function simplifies a common type * of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->getName(); * } * * You can express this more concisely with mpull(): * * $names = mpull($objects, 'getName'); * * mpull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->getID()] = $object->getName(); * } * * This is the mpull version(): * * $names = mpull($objects, 'getName', 'getID'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->getID()] = $object; * } * * With mpull(): * * $id_map = mpull($objects, null, 'getID'); * * See also @{function:ipull}, which works similarly but accesses array indexes * instead of calling methods. * * @param list Some list of objects. * @param string|null Determines which **values** will appear in the result * array. Use a string like 'getName' to store the * value of calling the named method in each value, or * ##null## to preserve the original objects. * @param string|null Determines how **keys** will be assigned in the result * array. Use a string like 'getID' to use the result * of calling the named method as each object's key, or * ##null## to preserve the original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as $method and $key_method. * @group util */ function mpull(array $list, $method, $key_method = null) { $result = array(); foreach ($list as $key => $object) { if ($key_method !== null) { $key = $object->$key_method(); } if ($method !== null) { $value = $object->$method(); } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Choose an index from a list of arrays. Short for "index pull", this function * works just like @{function:mpull}, except that it operates on a list of * arrays and selects an index from them instead of operating on a list of * objects and calling a method on them. * * This function simplifies a common type of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($list as $key => $dict) { * $names[$key] = $dict['name']; * } * * With ipull(): * * $names = ipull($list, 'name'); * * See @{function:mpull} for more usage examples. * * @param list Some list of arrays. * @param scalar|null Determines which **values** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the arrays unmodified as * values. * @param scalar|null Determines which **keys** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the array keys. * @return dict A dictionary with keys and values derived according * to whatever you passed for $index and $key_index. * @group util */ function ipull(array $list, $index, $key_index = null) { $result = array(); foreach ($list as $key => $array) { if ($key_index !== null) { $key = $array[$key_index]; } if ($index !== null) { $value = $array[$index]; } else { $value = $array; } $result[$key] = $value; } return $result; } /** * Group a list of objects by the result of some method, similar to how * GROUP BY works in an SQL query. This function simplifies grouping objects * by some property: * * COUNTEREXAMPLE * $animals_by_species = array(); * foreach ($animals as $animal) { * $animals_by_species[$animal->getSpecies()][] = $animal; * } * * This can be expressed more tersely with mgroup(): * * $animals_by_species = mgroup($animals, 'getSpecies'); * * In either case, the result is a dictionary which maps species (e.g., like * "dog") to lists of animals with that property, so all the dogs are grouped * together and all the cats are grouped together, or whatever super * businessesey thing is actually happening in your problem domain. * * See also @{function:igroup}, which works the same way but operates on * array indexes. * * @param list List of objects to group by some property. * @param string Name of a method, like 'getType', to call on each object * in order to determine which group it should be placed into. * @param ... Zero or more additional method names, to subgroup the * groups. * @return dict Dictionary mapping distinct method returns to lists of * all objects which returned that value. * @group util */ function mgroup(array $list, $by /* , ... */) { $map = mpull($list, $by); $groups = array(); foreach ($map as $group) { // Can't array_fill_keys() here because 'false' gets encoded wrong. $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('mgroup', $args); } } return $groups; } /** * Group a list of arrays by the value of some index. This function is the same * as @{function:mgroup}, except it operates on the values of array indexes * rather than the return values of method calls. * * @param list List of arrays to group by some index value. * @param string Name of an index to select from each array in order to * determine which group it should be placed into. * @param ... Zero or more additional indexes names, to subgroup the * groups. * @return dict Dictionary mapping distinct index values to lists of * all objects which had that value at the index. * @group util */ function igroup(array $list, $by /* , ... */) { $map = ipull($list, $by); $groups = array(); foreach ($map as $group) { $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('igroup', $args); } } return $groups; } /** * Sort a list of objects by the return value of some method. In PHP, this is * often vastly more efficient than ##usort()## and similar. * * // Sort a list of Duck objects by name. * $sorted = msort($ducks, 'getName'); * * It is usually significantly more efficient to define an ordering method * on objects and call ##msort()## than to write a comparator. It is often more * convenient, as well. * * NOTE: This method does not take the list by reference; it returns a new list. * * @param list List of objects to sort by some property. * @param string Name of a method to call on each object; the return values * will be used to sort the list. * @return list Objects ordered by the return values of the method calls. * @group util */ function msort(array $list, $method) { $surrogate = mpull($list, $method); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of arrays by the value of some index. This method is identical to * @{function:msort}, but operates on a list of arrays instead of a list of * objects. * * @param list List of arrays to sort by some index value. * @param string Index to access on each object; the return values * will be used to sort the list. * @return list Arrays ordered by the index values. * @group util */ function isort(array $list, $index) { $surrogate = ipull($list, $index); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Filter a list of objects by executing a method across all the objects and * filter out the ones wth empty() results. this function works just like * @{function:ifilter}, except that it operates on a list of objects instead * of a list of arrays. * * For example, to remove all objects with no children from a list, where * 'hasChildren' is a method name, do this: * * mfilter($list, 'hasChildren'); * * The optional third parameter allows you to negate the operation and filter * out nonempty objects. To remove all objects that DO have children, do this: * * mfilter($list, 'hasChildren', true); * * @param array List of objects to filter. * @param string A method name. * @param bool Optionally, pass true to drop objects which pass the * filter instead of keeping them. * * @return array List of objects which pass the filter. * @group util */ function mfilter(array $list, $method, $negate = false) { if (!is_string($method)) { throw new InvalidArgumentException('Argument method is not a string.'); } $result = array(); foreach ($list as $key => $object) { $value = $object->$method(); if (!$negate) { if (!empty($value)) { $result[$key] = $object; } } else { if (empty($value)) { $result[$key] = $object; } } } return $result; } /** * Filter a list of arrays by removing the ones with an empty() value for some * index. This function works just like @{function:mfilter}, except that it * operates on a list of arrays instead of a list of objects. * * For example, to remove all arrays without value for key 'username', do this: * * ifilter($list, 'username'); * * The optional third parameter allows you to negate the operation and filter * out nonempty arrays. To remove all arrays that DO have value for key * 'username', do this: * * ifilter($list, 'username', true); * * @param array List of arrays to filter. * @param scalar The index. * @param bool Optionally, pass true to drop arrays which pass the * filter instead of keeping them. * * @return array List of arrays which pass the filter. * @group util */ function ifilter(array $list, $index, $negate = false) { if (!is_scalar($index)) { throw new InvalidArgumentException('Argument index is not a scalar.'); } $result = array(); if (!$negate) { foreach ($list as $key => $array) { if (!empty($array[$index])) { $result[$key] = $array; } } } else { foreach ($list as $key => $array) { if (empty($array[$index])) { $result[$key] = $array; } } } return $result; } /** * Selects a list of keys from an array, returning a new array with only the * key-value pairs identified by the selected keys, in the specified order. * * Note that since this function orders keys in the result according to the * order they appear in the list of keys, there are effectively two common * uses: either reducing a large dictionary to a smaller one, or changing the * key order on an existing dictionary. * * @param dict Dictionary of key-value pairs to select from. * @param list List of keys to select. * @return dict Dictionary of only those key-value pairs where the key was * present in the list of keys to select. Ordering is * determined by the list order. * @group util */ function array_select_keys(array $dict, array $keys) { $result = array(); foreach ($keys as $key) { if (array_key_exists($key, $dict)) { $result[$key] = $dict[$key]; } } return $result; } /** * Checks if all values of array are instances of the passed class. * Throws InvalidArgumentException if it isn't true for any value. * * @param array * @param string * @return array Returns passed array. * @group util */ function assert_instances_of(array $arr, $class) { foreach ($arr as $key => $object) { if (!($object instanceof $class)) { $given = gettype($object); if (is_object($object)) { $given = 'instance of '.get_class($object); } throw new InvalidArgumentException( "Array item with key '{$key}' must be an instance of {$class}, ". "{$given} given." ); } } return $arr; } /** * Returns the first argument which is not strictly null, or ##null## if there * are no such arguments. Identical to the MySQL function of the same name. * * @param ... Zero or more arguments of any type. * @return mixed First non-##null## arg, or null if no such arg exists. * @group util */ function coalesce(/* ... */) { $args = func_get_args(); foreach ($args as $arg) { if ($arg !== null) { return $arg; } } return null; } /** * Similar to @{function:coalesce}, but less strict: returns the first * non-##empty()## argument, instead of the first argument that is strictly * non-##null##. If no argument is nonempty, it returns the last argument. This * is useful idiomatically for setting defaults: * * $display_name = nonempty($user_name, $full_name, "Anonymous"); * * @param ... Zero or more arguments of any type. * @return mixed First non-##empty()## arg, or last arg if no such arg * exists, or null if you passed in zero args. * @group util */ function nonempty(/* ... */) { $args = func_get_args(); $result = null; foreach ($args as $arg) { $result = $arg; if ($arg) { break; } } return $result; } /** * Invokes the "new" operator with a vector of arguments. There is no way to * call_user_func_array() on a class constructor, so you can instead use this * function: * * $obj = newv($class_name, $argv); * * That is, these two statements are equivalent: * * $pancake = new Pancake('Blueberry', 'Maple Syrup', true); * $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true)); * * DO NOT solve this problem in other, more creative ways! Three popular * alternatives are: * * - Build a fake serialized object and unserialize it. * - Invoke the constructor twice. * - just use eval() lol * * These are really bad solutions to the problem because they can have side * effects (e.g., __wakeup()) and give you an object in an otherwise impossible * state. Please endeavor to keep your objects in possible states. * * If you own the classes you're doing this for, you should consider whether * or not restructuring your code (for instance, by creating static * construction methods) might make it cleaner before using newv(). Static * constructors can be invoked with call_user_func_array(), and may give your * class a cleaner and more descriptive API. * * @param string The name of a class. * @param list Array of arguments to pass to its constructor. * @return obj A new object of the specified class, constructed by passing * the argument vector to its constructor. * @group util */ function newv($class_name, array $argv) { $reflector = new ReflectionClass($class_name); if ($argv) { return $reflector->newInstanceArgs($argv); } else { return $reflector->newInstance(); } } /** * Returns the first element of an array. Exactly like reset(), but doesn't * choke if you pass it some non-referenceable value like the return value of * a function. * * @param array Array to retrieve the first element from. * @return wild The first value of the array. * @group util */ function head(array $arr) { return reset($arr); } /** * Returns the last element of an array. This is exactly like end() except * that it won't warn you if you pass some non-referencable array to * it -- e.g., the result of some other array operation. * * @param array Array to retrieve the last element from. * @return wild The last value of the array. * @group util */ function last(array $arr) { return end($arr); } /** * Returns the first key of an array. * * @param array Array to retrieve the first key from. * @return int|string The first key of the array. * @group util */ function head_key(array $arr) { reset($arr); return key($arr); } /** * Returns the last key of an array. * * @param array Array to retrieve the last key from. * @return int|string The last key of the array. * @group util */ function last_key(array $arr) { end($arr); return key($arr); } /** * Merge a vector of arrays performantly. This has the same semantics as * array_merge(), so these calls are equivalent: * * array_merge($a, $b, $c); * array_mergev(array($a, $b, $c)); * * However, when you have a vector of arrays, it is vastly more performant to * merge them with this function than by calling array_merge() in a loop, * because using a loop generates an intermediary array on each iteration. * * @param list Vector of arrays to merge. * @return list Arrays, merged with array_merge() semantics. * @group util */ function array_mergev(array $arrayv) { if (!$arrayv) { return array(); } return call_user_func_array('array_merge', $arrayv); } /** * Split a corpus of text into lines. This function splits on "\n", "\r\n", or * a mixture of any of them. * * NOTE: This function does not treat "\r" on its own as a newline because none * of SVN, Git or Mercurial do on any OS. * * @param string Block of text to be split into lines. * @param bool If true, retain line endings in result strings. * @return list List of lines. * @group util */ function phutil_split_lines($corpus, $retain_endings = true) { if (!strlen($corpus)) { return array(''); } // Split on "\r\n" or "\n". if ($retain_endings) { $lines = preg_split('/(?<=\n)/', $corpus); } else { $lines = preg_split('/\r?\n/', $corpus); } // If the text ends with "\n" or similar, we'll end up with an empty string // at the end; discard it. if (end($lines) == '') { array_pop($lines); } return $lines; } diff --git a/src/xsprintf/csprintf.php b/src/xsprintf/csprintf.php index d5105d8..e71ab6f 100644 --- a/src/xsprintf/csprintf.php +++ b/src/xsprintf/csprintf.php @@ -1,94 +1,78 @@ $pos + 1) ? $pattern[$pos + 1] : null; switch ($type) { case 'L': // Only '%Ls' is supported. if ($next !== 's') { throw new Exception("Unknown conversion %L{$next}."); } // Remove the L, leaving %s $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; // Check that the value is a non-empty array. if (!is_array($value) || !$value) { throw new Exception("Expected a non-empty array for %Ls conversion."); } // Convert the list of strings to a single string. $value = implode(' ', array_map('escapeshellarg', $value)); break; case 's': $value = escapeshellarg($value); $type = 's'; break; case 'C': $type = 's'; break; } $pattern[$pos] = $type; } diff --git a/src/xsprintf/jsprintf.php b/src/xsprintf/jsprintf.php index 0f81e3c..a8b34c1 100644 --- a/src/xsprintf/jsprintf.php +++ b/src/xsprintf/jsprintf.php @@ -1,112 +1,96 @@ 0x1FFFFFFFFFFFFF) { throw new Exception( "You are passing an integer to jsprintf() which is so large it can ". "not be represented without loss of precision by Javascript's ". "native Number class. Use %# instead."); } break; } $pattern[$pos] = $type; } diff --git a/src/xsprintf/ldapsprintf.php b/src/xsprintf/ldapsprintf.php index d235ec6..a0475c5 100644 --- a/src/xsprintf/ldapsprintf.php +++ b/src/xsprintf/ldapsprintf.php @@ -1,66 +1,50 @@ ;"= '); $type = 's'; break; case 'Q': $type = 's'; break; } $pattern[$pos] = $type; } diff --git a/src/xsprintf/qsprintf.php b/src/xsprintf/qsprintf.php index a3e7003..21543a2 100644 --- a/src/xsprintf/qsprintf.php +++ b/src/xsprintf/qsprintf.php @@ -1,314 +1,298 @@ and %<. * * %> ("Prefix") * Escapes a prefix query for a LIKE clause. For example: * * // Find all rows where `name` starts with $prefix. * qsprintf($conn, 'WHERE name LIKE %>', $prefix); * * %< ("Suffix") * Escapes a suffix query for a LIKE clause. For example: * * // Find all rows where `name` ends with $suffix. * qsprintf($conn, 'WHERE name LIKE %<', $suffix); * * @group storage */ function qsprintf(AphrontDatabaseConnection $conn, $pattern/* , ... */) { $args = func_get_args(); array_shift($args); return xsprintf('xsprintf_query', $conn, $args); } /** * @group storage */ function vqsprintf(AphrontDatabaseConnection $conn, $pattern, array $argv) { array_unshift($argv, $pattern); return xsprintf('xsprintf_query', $conn, $argv); } /** * xsprintf() callback for encoding SQL queries. See qsprintf(). * @group storage */ function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) { $type = $pattern[$pos]; $conn = $userdata; $next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null; $nullable = false; $done = false; $prefix = ''; if (!($conn instanceof AphrontDatabaseConnection)) { throw new Exception("Invalid database connection!"); } switch ($type) { case '=': // Nullable test switch ($next) { case 'd': case 'f': case 's': $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; if ($value === null) { $value = 'IS NULL'; $done = true; } else { $prefix = '= '; $type = $next; } break; default: throw new Exception('Unknown conversion, try %=d, %=s, or %=f.'); } break; case 'n': // Nullable... switch ($next) { case 'd': // ...integer. case 'f': // ...float. case 's': // ...string. $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = $next; $nullable = true; break; default: throw new Exception('Unknown conversion, try %nd or %ns.'); } break; case 'L': // List of.. _qsprintf_check_type($value, "L{$next}", $pattern); $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; $done = true; switch ($next) { case 'd': // ...integers. $value = implode(', ', array_map('intval', $value)); break; case 's': // ...strings. foreach ($value as $k => $v) { $value[$k] = "'".$conn->escapeString($v)."'"; } $value = implode(', ', $value); break; case 'C': // ...columns. foreach ($value as $k => $v) { $value[$k] = $conn->escapeColumnName($v); } $value = implode(', ', $value); break; default: throw new Exception("Unknown conversion %L{$next}."); } break; } if (!$done) { _qsprintf_check_type($value, $type, $pattern); switch ($type) { case 's': // String if ($nullable && $value === null) { $value = 'NULL'; } else { $value = "'".$conn->escapeString($value)."'"; } $type = 's'; break; case 'Q': // Query Fragment $type = 's'; break; case '~': // Like Substring case '>': // Like Prefix case '<': // Like Suffix $value = $conn->escapeStringForLikeClause($value); switch ($type) { case '~': $value = "'%".$value."%'"; break; case '>': $value = "'" .$value."%'"; break; case '<': $value = "'%".$value. "'"; break; } $type = 's'; break; case 'f': // Float if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (float)$value; } $type = 's'; break; case 'd': // Integer if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (int)$value; } $type = 's'; break; case 'T': // Table case 'C': // Column $value = $conn->escapeColumnName($value); $type = 's'; break; case 'K': // Komment $value = $conn->escapeMultilineComment($value); $type = 's'; break; default: throw new Exception("Unknown conversion '%{$type}'."); } } if ($prefix) { $value = $prefix.$value; } $pattern[$pos] = $type; } /** * @group storage */ function _qsprintf_check_type($value, $type, $query) { switch ($type) { case 'Ld': case 'Ls': case 'LC': case 'LA': case 'LO': if (!is_array($value)) { throw new AphrontQueryParameterException( $query, "Expected array argument for %{$type} conversion."); } if (empty($value)) { throw new AphrontQueryParameterException( $query, "Array for %{$type} conversion is empty."); } foreach ($value as $scalar) { _qsprintf_check_scalar_type($scalar, $type, $query); } break; default: _qsprintf_check_scalar_type($value, $type, $query); break; } } /** * @group storage */ function _qsprintf_check_scalar_type($value, $type, $query) { switch ($type) { case 'Q': case 'LC': case 'T': case 'C': if (!is_string($value)) { throw new AphrontQueryParameterException( $query, "Expected a string for %{$type} conversion."); } break; case 'Ld': case 'd': case 'f': if (!is_null($value) && !is_numeric($value)) { throw new AphrontQueryParameterException( $query, "Expected a numeric scalar or null for %{$type} conversion."); } break; case 'Ls': case 's': case '~': case '>': case '<': case 'K': if (!is_null($value) && !is_scalar($value)) { throw new AphrontQueryParameterException( $query, "Expected a scalar or null for %{$type} conversion."); } break; case 'LA': case 'LO': if (!is_null($value) && !is_scalar($value) && !(is_array($value) && !empty($value))) { throw new AphrontQueryParameterException( $query, "Expected a scalar or null or non-empty array for ". "%{$type} conversion."); } break; default: throw new Exception("Unknown conversion '{$type}'."); } } diff --git a/src/xsprintf/queryfx.php b/src/xsprintf/queryfx.php index 3e460e0..b94cf2a 100644 --- a/src/xsprintf/queryfx.php +++ b/src/xsprintf/queryfx.php @@ -1,67 +1,51 @@ executeRawQuery($query); } /** * @group storage */ function vqueryfx(AphrontDatabaseConnection $conn, $sql, array $argv) { array_unshift($argv, $conn, $sql); call_user_func_array('queryfx', $argv); } /** * @group storage */ function queryfx_all(AphrontDatabaseConnection $conn, $sql/* , ... */) { $argv = func_get_args(); call_user_func_array('queryfx', $argv); return $conn->selectAllResults(); } /** * @group storage */ function queryfx_one(AphrontDatabaseConnection $conn, $sql/* , ... */) { $argv = func_get_args(); $ret = call_user_func_array('queryfx_all', $argv); if (count($ret) > 1) { throw new AphrontQueryCountException( 'Query returned more than one row.'); } else if (count($ret)) { return reset($ret); } return null; } /** * @group storage */ function vqueryfx_all(AphrontDatabaseConnection $conn, $sql, array $argv) { array_unshift($argv, $conn, $sql); call_user_func_array('queryfx', $argv); return $conn->selectAllResults(); } diff --git a/src/xsprintf/xsprintf.php b/src/xsprintf/xsprintf.php index 5fccb51..5cc97e8 100644 --- a/src/xsprintf/xsprintf.php +++ b/src/xsprintf/xsprintf.php @@ -1,136 +1,120 @@ = $argc) { throw new Exception("Too few arguments to xsprintf()."); } $callback($userdata, $pattern, $pos, $argv[$arg], $len); } } if ($c == '%') { // If we have "%%", this encodes a literal percentage symbol, so we are // no longer inside a conversion. $conv = !$conv; } } if ($arg != ($argc - 1)) { throw new Exception("Too many arguments to xsprintf()."); } $argv[0] = $pattern; return call_user_func_array('sprintf', $argv); } /** * Example @{function:xsprintf} callback. When you call xsprintf(), you * must pass a callback like this one. xsprintf() will invoke the callback when * it encounters a conversion (like "%Z") in the pattern string. * * Generally, this callback should examine ##$pattern[$pos]## (which will * contain the conversion character, like 'Z'), escape ##$value## appropriately, * and then replace ##$pattern[$pos]## with an 's' so sprintf() prints the * escaped value as a string. However, more sophisticated behaviors are possible * -- particularly, consuming multiple characters to allow for conversions like * "%Ld". In this case, the callback needs to substr_replace() the entire * conversion with 's' and then update ##$length##. * * For example implementations, see @{function:xsprintf_command}, * @{function:xsprintf_javascript}, * and @{function:xsprintf_query}. * * @param wild Arbitrary, optional userdata. This is whatever userdata * was passed to @{function:xsprintf}. * @param string The pattern string being parsed. * @param int The current character position in the string. * @param wild The value to convert. * @param int The string length. * * @group util */ function xsprintf_callback_example( $userdata, &$pattern, &$pos, &$value, &$length) { throw new Exception( "This function exists only to document the call signature for xsprintf() ". "callbacks."); } diff --git a/support/xhpast/ast.hpp b/support/xhpast/ast.hpp index 7548b9a..a84a839 100644 --- a/support/xhpast/ast.hpp +++ b/support/xhpast/ast.hpp @@ -1,101 +1,85 @@ -/* - * 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. - */ - #pragma once #include #include #include #include #include "astnode.hpp" class yy_extra_type { public: yy_extra_type() { lineno = 1; terminated = false; used = false; short_tags = true; asp_tags = false; idx_expr = false; include_debug = false; expecting_xhp_class_statements = false; list_size = 0; colon_hack = false; pushStack(); } bool short_tags; // `short_open_tag` in php.ini bool asp_tags; // `asp_tags` in php.ini bool idx_expr; // allow code like `foo()['bar']` bool include_debug; // include line numbers and file names in XHP object creation size_t first_lineno; // line number before scanning the current token size_t lineno; // current line number being scanned. std::string error; // description of error (if terminated true) bool terminated; // becomes true when the parser terminates with an error bool used; // were any XHP-specific extensions found in this code? int last_token; // the last token to be returned by the scanner int insert_token; // insert this token without reading from buffer size_t heredoc_yyleng; // last length of yytext while scannling const char* heredoc_data; // where our heredoc data starts std::string heredoc_label; // heredoc sentinel label std::stack curly_stack; // tokens appearing before a { bool expecting_xhp_class_statements; // when we're one level deep in a class bool old_expecting_xhp_class_statements; // store old value while inside class method bool used_attributes; // did this class use the `attribute` keyword unsigned int list_size; bool colon_hack; xhpast::token_list_t token_list; /* Utility functions for checking proper tag closing */ bool haveTag() { return !tag_stack.front().empty(); } const std::string &peekTag() { return tag_stack.front().front(); } void pushTag(const std::string &tag) { tag_stack.front().push_front(tag); } void popTag() { tag_stack.front().pop_front(); } void pushStack() { tag_stack.push_front(std::deque()); } void popStack() { tag_stack.pop_front(); } protected: std::deque > tag_stack; }; #define YYSTYPE xhpast::Node * #define YY_HEADER_EXPORT_START_CONDITIONS #define YY_EXTRA_TYPE yy_extra_type* #include "parser.yacc.hpp" #ifndef FLEX_SCANNER #include "scanner.lex.hpp" #endif int xhpparse(void*, YYSTYPE *); void xhp_new_push_state(int s, struct yyguts_t* yyg); void xhp_new_pop_state(struct yyguts_t* yyg); void xhp_set_state(int s, struct yyguts_t* yyg); diff --git a/support/xhpast/astnode.cpp b/support/xhpast/astnode.cpp index ac3ece2..4c43418 100644 --- a/support/xhpast/astnode.cpp +++ b/support/xhpast/astnode.cpp @@ -1,17 +1 @@ -/* - * 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. - */ - #include "astnode.hpp" diff --git a/support/xhpast/astnode.hpp b/support/xhpast/astnode.hpp index 463d42e..473916f 100644 --- a/support/xhpast/astnode.hpp +++ b/support/xhpast/astnode.hpp @@ -1,129 +1,113 @@ -/* - * 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. - */ - #pragma once #include #include #include #include namespace xhpast { class Token; typedef std::list token_list_t; class Token { public: unsigned int type; std::string value; unsigned int lineno; unsigned int n; Token(unsigned int type, char *value, unsigned int n) : type(type), value(value), n(n) { } }; class Node; typedef std::list node_list_t; class Node { public: unsigned int type; int l_tok; int r_tok; node_list_t children; Node() : type(0), l_tok(-1), r_tok(-1) {}; Node(unsigned int type) : type(type), l_tok(-1), r_tok(-1) {}; Node(unsigned int type, int end_tok) : type(type) { this->l_tok = end_tok; this->r_tok = end_tok; } Node(unsigned int type, int l_tok, int r_tok) : type(type), l_tok(l_tok), r_tok(r_tok) { } Node *appendChild(Node *node) { this->children.push_back(node); return this->setEnd(node); } Node *appendChildren(Node *node) { for (node_list_t::iterator ii = node->children.begin(); ii != node->children.end(); ++ii) { this->children.push_back(*ii); this->setEnd(*ii); } return this; } Node *firstChild() { if (this->children.empty()) { return NULL; } return *(this->children.begin()); } Node *setType(unsigned int t) { this->type = t; return this; } Node *setEnd(Node *n) { if (!n) { fprintf(stderr, "Trying to setEnd() a null node to one of type %d\n", this->type); exit(1); } if (n->r_tok != -1 && (n->r_tok > this->r_tok || (this->r_tok == -1))) { this->r_tok = n->r_tok; } if (this->l_tok == -1) { this->l_tok = n->l_tok; } return this; } Node *setBegin(Node *n) { if (!n) { fprintf(stderr, "Trying to setBegin() a null node to one of type %d\n", this->type); exit(1); } if (n->l_tok != -1 && (n->l_tok < this->l_tok || (this->l_tok == -1))) { this->l_tok = n->l_tok; } if (this->r_tok == -1) { this->r_tok = n->r_tok; } return this; } }; } diff --git a/support/xhpast/generate_nodes.php b/support/xhpast/generate_nodes.php index ad2fc3d..cd08437 100755 --- a/support/xhpast/generate_nodes.php +++ b/support/xhpast/generate_nodes.php @@ -1,156 +1,140 @@ #!/usr/local/bin/php $value) { $hpp .= "#define {$node} {$value}\n"; } file_put_contents('node_names.hpp', $hpp); echo "Wrote C++ definition.\n"; $at = '@'; $php = " $value) { $php .= " {$value} => '{$node}',\n"; } $php .= " );\n"; $php .= "}\n"; file_put_contents('parser_nodes.php', $php); echo "Wrote PHP definition.\n"; diff --git a/support/xhpast/parser.y b/support/xhpast/parser.y index 8894ba7..c03af06 100644 --- a/support/xhpast/parser.y +++ b/support/xhpast/parser.y @@ -1,2440 +1,2424 @@ -/* - * Copyright 2012 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. - */ - %{ /* * If you modify this grammar, please update the version number in * ./xhpast.cpp and libphutil/src/parser/xhpast/bin/xhpast_parse.php */ #include "ast.hpp" #include "node_names.hpp" // PHP's if/else rules use right reduction rather than left reduction which // means while parsing nested if/else's the stack grows until it the last // statement is read. This is annoying, particularly because of a quirk in // bison. // http://www.gnu.org/software/bison/manual/html_node/Memory-Management.html // Apparently if you compile a bison parser with g++ it can no longer grow // the stack. The work around is to just make your initial stack ridiculously // large. Unfortunately that increases memory usage while parsing which is // dumb. Anyway, putting a TODO here to fix PHP's if/else grammar. #define YYINITDEPTH 500 %} %{ #undef yyextra #define yyextra static_cast(xhpastget_extra(yyscanner)) #undef yylineno #define yylineno yyextra->first_lineno #define push_state(s) xhp_new_push_state(s, (struct yyguts_t*) yyscanner) #define pop_state() xhp_new_pop_state((struct yyguts_t*) yyscanner) #define set_state(s) xhp_set_state(s, (struct yyguts_t*) yyscanner) #define NNEW(t) \ (new xhpast::Node(t)) #define NTYPE(n, type) \ ((n)->setType(type)) #define NMORE(n, end) \ ((n)->setEnd(end)) #define NSPAN(n, type, end) \ (NMORE(NTYPE((n), type), end)) #define NLMORE(n, begin) \ ((n)->setBegin(begin)) #define NEXPAND(l, n, r) \ ((n)->setBegin(l)->setEnd(r)) using namespace std; static void yyerror(void* yyscanner, void* _, const char* error) { if (yyextra->terminated) { return; } yyextra->terminated = true; yyextra->error = error; } /* TODO: Restore this. static void replacestr(string &source, const string &find, const string &rep) { size_t j; while ((j = source.find(find)) != std::string::npos) { source.replace(j, find.length(), rep); } } */ %} %expect 9 // 2: PHP's if/else grammar // 7: expr '[' dim_offset ']' -- shift will default to first grammar %name-prefix = "xhpast" %pure-parser %parse-param { void* yyscanner } %parse-param { xhpast::Node** root } %lex-param { void* yyscanner } %error-verbose %left T_INCLUDE T_INCLUDE_ONCE T_EVAL T_REQUIRE T_REQUIRE_ONCE %left ',' %left T_LOGICAL_OR %left T_LOGICAL_XOR %left T_LOGICAL_AND %right T_PRINT %left '=' T_PLUS_EQUAL T_MINUS_EQUAL T_MUL_EQUAL T_DIV_EQUAL T_CONCAT_EQUAL T_MOD_EQUAL T_AND_EQUAL T_OR_EQUAL T_XOR_EQUAL T_SL_EQUAL T_SR_EQUAL %left '?' ':' %left T_BOOLEAN_OR %left T_BOOLEAN_AND %left '|' %left '^' %left '&' %nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL %nonassoc '<' T_IS_SMALLER_OR_EQUAL '>' T_IS_GREATER_OR_EQUAL %left T_SL T_SR %left '+' '-' '.' %left '*' '/' '%' %right '!' %nonassoc T_INSTANCEOF %right '~' T_INC T_DEC T_INT_CAST T_DOUBLE_CAST T_STRING_CAST T_UNICODE_CAST T_BINARY_CAST T_ARRAY_CAST T_OBJECT_CAST T_BOOL_CAST T_UNSET_CAST '@' %right '[' %nonassoc T_NEW T_CLONE %token T_EXIT %token T_IF %left T_ELSEIF %left T_ELSE %left T_ENDIF %token T_LNUMBER %token T_DNUMBER %token T_STRING %token T_STRING_VARNAME /* unused in XHP: `foo` in `"$foo"` */ %token T_VARIABLE %token T_NUM_STRING /* unused in XHP: `0` in `"$foo[0]"` */ %token T_INLINE_HTML %token T_CHARACTER /* unused in vanilla PHP */ %token T_BAD_CHARACTER /* unused in vanilla PHP */ %token T_ENCAPSED_AND_WHITESPACE /* unused in XHP: ` ` in `" "` */ %token T_CONSTANT_ENCAPSED_STRING /* overloaded in XHP; replaces '"' encaps_list '"' */ %token T_BACKTICKS_EXPR /* new in XHP; replaces '`' backticks_expr '`' */ %token T_ECHO %token T_DO %token T_WHILE %token T_ENDWHILE %token T_FOR %token T_ENDFOR %token T_FOREACH %token T_ENDFOREACH %token T_DECLARE %token T_ENDDECLARE %token T_AS %token T_SWITCH %token T_ENDSWITCH %token T_CASE %token T_DEFAULT %token T_BREAK %token T_CONTINUE %token T_GOTO %token T_FUNCTION %token T_CONST %token T_RETURN %token T_TRY %token T_CATCH %token T_THROW %token T_USE %token T_GLOBAL %right T_STATIC T_ABSTRACT T_FINAL T_PRIVATE T_PROTECTED T_PUBLIC %token T_VAR %token T_UNSET %token T_ISSET %token T_EMPTY %token T_HALT_COMPILER %token T_CLASS %token T_INTERFACE %token T_EXTENDS %token T_IMPLEMENTS %token T_OBJECT_OPERATOR %token T_DOUBLE_ARROW %token T_LIST %token T_ARRAY %token T_CLASS_C %token T_METHOD_C %token T_FUNC_C %token T_LINE %token T_FILE %token T_COMMENT %token T_DOC_COMMENT %token T_OPEN_TAG %token T_OPEN_TAG_WITH_ECHO %token T_OPEN_TAG_FAKE %token T_CLOSE_TAG %token T_WHITESPACE %token T_START_HEREDOC /* unused in XHP; replaced with T_HEREDOC */ %token T_END_HEREDOC /* unused in XHP; replaced with T_HEREDOC */ %token T_HEREDOC /* new in XHP; replaces start_heredoc encaps_list T_END_HEREDOC */ %token T_DOLLAR_OPEN_CURLY_BRACES /* unused in XHP: `${` in `"${foo}"` */ %token T_CURLY_OPEN /* unused in XHP: `{$` in `"{$foo}"` */ %token T_PAAMAYIM_NEKUDOTAYIM %token T_BINARY_DOUBLE /* unsused in XHP: `b"` in `b"foo"` */ %token T_BINARY_HEREDOC /* unsused in XHP: `b<<<` in `b<<appendChild($1); } ; top_statement_list: top_statement_list top_statement { $$ = $1->appendChild($2); } | /* empty */ { $$ = NNEW(n_STATEMENT_LIST); } ; namespace_name: T_STRING { $$ = NTYPE($1, n_SYMBOL_NAME); } | namespace_name T_NS_SEPARATOR T_STRING { $$ = NMORE($1, $3); } ; top_statement: statement | function_declaration_statement | class_declaration_statement | T_HALT_COMPILER '(' ')' ';' { $1 = NSPAN($1, n_HALT_COMPILER, $3); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $4); } | T_NAMESPACE namespace_name ';' { NSPAN($1, n_NAMESPACE, $2); $1->appendChild($2); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_NAMESPACE namespace_name '{' top_statement_list '}' { NSPAN($1, n_NAMESPACE, $5); $1->appendChild($2); $1->appendChild(NEXPAND($3, $4, $5)); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_NAMESPACE '{' top_statement_list '}' { NSPAN($1, n_NAMESPACE, $4); $1->appendChild(NNEW(n_EMPTY)); NMORE($3, $4); NLMORE($3, $2); $1->appendChild($3); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_USE use_declarations ';' { NSPAN($1, n_USE, $2); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | constant_declaration ';' { $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } ; use_declarations: use_declarations ',' use_declaration { $$ = $1->appendChild($3); } | use_declaration { $$ = NNEW(n_USE_LIST); $$->appendChild($1); } ; use_declaration: namespace_name { $$ = NNEW(n_USE); $$->appendChild($1); $$->appendChild(NNEW(n_EMPTY)); } | namespace_name T_AS T_STRING { $$ = NNEW(n_USE); $$->appendChild($1); NTYPE($3, n_STRING); $$->appendChild($3); } | T_NS_SEPARATOR namespace_name { $$ = NNEW(n_USE); NLMORE($2, $1); $$->appendChild($2); $$->appendChild(NNEW(n_EMPTY)); } | T_NS_SEPARATOR namespace_name T_AS T_STRING { $$ = NNEW(n_USE); NLMORE($2, $1); $$->appendChild($2); NTYPE($4, n_STRING); $$->appendChild($4); } ; constant_declaration: constant_declaration ',' T_STRING '=' static_scalar { NMORE($$, $5); $$->appendChild( NNEW(n_CONSTANT_DECLARATION) ->appendChild(NTYPE($3, n_STRING)) ->appendChild($5)); } | T_CONST T_STRING '=' static_scalar { NSPAN($$, n_CONSTANT_DECLARATION_LIST, $4); $$->appendChild( NNEW(n_CONSTANT_DECLARATION) ->appendChild(NTYPE($2, n_STRING)) ->appendChild($4)); } ; inner_statement_list: inner_statement_list inner_statement { $$ = $1->appendChild($2); } | /* empty */ { $$ = NNEW(n_STATEMENT_LIST); } ; inner_statement: statement | function_declaration_statement | class_declaration_statement | T_HALT_COMPILER '(' ')' ';' { $1 = NSPAN($1, n_HALT_COMPILER, $3); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $4); } ; statement: unticked_statement | T_STRING ':' { NTYPE($1, n_STRING); $$ = NNEW(n_LABEL); $$->appendChild($1); NMORE($$, $2); } | T_OPEN_TAG { $$ = NTYPE($1, n_OPEN_TAG); } | T_OPEN_TAG_WITH_ECHO { $$ = NTYPE($1, n_OPEN_TAG); } | T_CLOSE_TAG { $$ = NTYPE($1, n_CLOSE_TAG); } ; unticked_statement: '{' inner_statement_list '}' { $$ = NEXPAND($1, $2, $3); } | T_IF '(' expr ')' statement elseif_list else_single { $$ = NNEW(n_CONDITION_LIST); $1 = NTYPE($1, n_IF); $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); $1->appendChild($5); $$->appendChild($1); $$->appendChildren($6); // Hacks: merge a list of if (x) { } else if (y) { } into a single condition // list instead of a condition tree. if ($7->type == n_EMPTY) { // Ignore. } else if ($7->type == n_ELSE) { xhpast::Node *stype = $7->firstChild()->firstChild(); if (stype && stype->type == n_CONDITION_LIST) { NTYPE(stype->firstChild(), n_ELSEIF); stype->firstChild()->l_tok = $7->l_tok; $$->appendChildren(stype); } else { $$->appendChild($7); } } else { $$->appendChild($7); } $$ = NNEW(n_STATEMENT)->appendChild($$); } | T_IF '(' expr ')' ':' inner_statement_list new_elseif_list new_else_single T_ENDIF ';' { $$ = NNEW(n_CONDITION_LIST); NTYPE($1, n_IF); $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); $1->appendChild($6); $$->appendChild($1); $$->appendChildren($7); $$->appendChild($8); NMORE($$, $9); $$ = NNEW(n_STATEMENT)->appendChild($$); NMORE($$, $10); } | T_WHILE '(' expr ')' while_statement { NTYPE($1, n_WHILE); $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); $1->appendChild($5); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_DO statement T_WHILE '(' expr ')' ';' { NTYPE($1, n_DO_WHILE); $1->appendChild($2); $1->appendChild(NSPAN($4, n_CONTROL_CONDITION, $6)->appendChild($5)); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $7); } | T_FOR '(' for_expr ';' for_expr ';' for_expr ')' for_statement { NTYPE($1, n_FOR); NSPAN($2, n_FOR_EXPRESSION, $8) ->appendChild($3) ->appendChild($5) ->appendChild($7); $1->appendChild($2); $1->appendChild($9); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_SWITCH '(' expr ')' switch_case_list { NTYPE($1, n_SWITCH); $1->appendChild(NSPAN($2, n_CONTROL_CONDITION, $4)->appendChild($3)); $1->appendChild($5); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_BREAK ';' { NTYPE($1, n_BREAK); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } | T_BREAK expr ';' { NTYPE($1, n_BREAK); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_CONTINUE ';' { NTYPE($1, n_CONTINUE); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } | T_CONTINUE expr ';' { NTYPE($1, n_CONTINUE); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_RETURN ';' { NTYPE($1, n_RETURN); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } | T_RETURN expr_without_variable ';' { NTYPE($1, n_RETURN); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_RETURN variable ';' { NTYPE($1, n_RETURN); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_GLOBAL global_var_list ';' { NLMORE($2, $1); $$ = NNEW(n_STATEMENT)->appendChild($2); NMORE($$, $3); } | T_STATIC static_var_list ';' { NLMORE($2, $1); $$ = NNEW(n_STATEMENT)->appendChild($2); NMORE($$, $3); } | T_ECHO echo_expr_list ';' { NLMORE($2, $1); $$ = NNEW(n_STATEMENT)->appendChild($2); NMORE($$, $3); } | T_INLINE_HTML { NTYPE($1, n_INLINE_HTML); $$ = $1; } | expr ';' { $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } | T_UNSET '(' unset_variables ')' ';' { NMORE($3, $4); NLMORE($3, $1); $$ = NNEW(n_STATEMENT)->appendChild($3); NMORE($$, $5); } | T_FOREACH '(' variable T_AS foreach_variable foreach_optional_arg ')' foreach_statement { NTYPE($1, n_FOREACH); NSPAN($2, n_FOREACH_EXPRESSION, $7); $2->appendChild($3); if ($6->type == n_EMPTY) { $2->appendChild($6); $2->appendChild($5); } else { $2->appendChild($5); $2->appendChild($6); } $1->appendChild($2); $1->appendChild($8); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_FOREACH '(' expr_without_variable T_AS variable foreach_optional_arg ')' foreach_statement { NTYPE($1, n_FOREACH); NSPAN($2, n_FOREACH_EXPRESSION, $7); $2->appendChild($3); if ($6->type == n_EMPTY) { $2->appendChild($6); $2->appendChild($5); } else { $2->appendChild($5); $2->appendChild($6); } $1->appendChild($2); $1->appendChild($8); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_DECLARE '(' declare_list ')' declare_statement { NTYPE($1, n_DECLARE); $1->appendChild($3); $1->appendChild($5); $$ = NNEW(n_STATEMENT)->appendChild($1); } | ';' /* empty statement */ { $$ = NNEW(n_STATEMENT)->appendChild(NNEW(n_EMPTY)); NMORE($$, $1); } | T_TRY '{' inner_statement_list '}' T_CATCH '(' fully_qualified_class_name T_VARIABLE ')' '{' inner_statement_list '}' additional_catches { NTYPE($1, n_TRY); $1->appendChild(NEXPAND($2, $3, $4)); NTYPE($5, n_CATCH); $5->appendChild($7); $5->appendChild(NTYPE($8, n_VARIABLE)); $5->appendChild(NEXPAND($10, $11, $12)); $1->appendChild(NNEW(n_CATCH_LIST)->appendChild($5)->appendChildren($13)); $$ = NNEW(n_STATEMENT)->appendChild($1); } | T_THROW expr ';' { NTYPE($1, n_THROW); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } | T_GOTO T_STRING ';' { NTYPE($1, n_GOTO); NTYPE($2, n_STRING); $1->appendChild($2); $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $3); } ; additional_catches: non_empty_additional_catches | /* empty */ { $$ = NNEW(n_EMPTY); } ; non_empty_additional_catches: additional_catch { $$ = NNEW(n_CATCH_LIST); $$->appendChild($1); } | non_empty_additional_catches additional_catch { $1->appendChild($2); $$ = $1; } ; additional_catch: T_CATCH '(' fully_qualified_class_name T_VARIABLE ')' '{' inner_statement_list '}' { NTYPE($1, n_CATCH); $1->appendChild($3); $1->appendChild(NTYPE($4, n_VARIABLE)); $1->appendChild(NEXPAND($6, $7, $8)); NMORE($1, $8); $$ = $1; } ; unset_variables: unset_variable { $$ = NNEW(n_UNSET_LIST); $$->appendChild($1); } | unset_variables ',' unset_variable { $1->appendChild($3); $$ = $1; } ; unset_variable: variable ; function_declaration_statement: unticked_function_declaration_statement ; class_declaration_statement: unticked_class_declaration_statement ; is_reference: /* empty */ { $$ = NNEW(n_EMPTY); } | '&' { $$ = NTYPE($1, n_REFERENCE); } ; unticked_function_declaration_statement: function is_reference T_STRING '(' parameter_list ')' '{' inner_statement_list '}' { NSPAN($1, n_FUNCTION_DECLARATION, $9); $1->appendChild(NNEW(n_EMPTY)); $1->appendChild($2); $1->appendChild(NTYPE($3, n_STRING)); $1->appendChild(NEXPAND($4, $5, $6)); $$->appendChild(NNEW(n_EMPTY)); $1->appendChild(NEXPAND($7, $8, $9)); $$ = NNEW(n_STATEMENT)->appendChild($1); } ; unticked_class_declaration_statement: class_entry_type T_STRING extends_from implements_list '{' class_statement_list '}' { $$ = NNEW(n_CLASS_DECLARATION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_CLASS_NAME)); $$->appendChild($3); $$->appendChild($4); $$->appendChild(NEXPAND($5, $6, $7)); NMORE($$, $7); $$ = NNEW(n_STATEMENT)->appendChild($$); } | interface_entry T_STRING interface_extends_list '{' class_statement_list '}' { $$ = NNEW(n_INTERFACE_DECLARATION); $$->appendChild(NNEW(n_CLASS_ATTRIBUTES)); NLMORE($$, $1); $$->appendChild(NTYPE($2, n_CLASS_NAME)); $$->appendChild($3); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild(NEXPAND($4, $5, $6)); NMORE($$, $6); $$ = NNEW(n_STATEMENT)->appendChild($$); } ; class_entry_type: T_CLASS { NTYPE($1, n_CLASS_ATTRIBUTES); $$ = $1; } | T_ABSTRACT T_CLASS { NTYPE($2, n_CLASS_ATTRIBUTES); NLMORE($2, $1); $2->appendChild(NTYPE($1, n_STRING)); $$ = $2; } | T_FINAL T_CLASS { NTYPE($2, n_CLASS_ATTRIBUTES); NLMORE($2, $1); $2->appendChild(NTYPE($1, n_STRING)); $$ = $2; } ; extends_from: /* empty */ { $$ = NNEW(n_EMPTY); } | T_EXTENDS fully_qualified_class_name { $$ = NTYPE($1, n_EXTENDS_LIST)->appendChild($2); } ; interface_entry: T_INTERFACE ; interface_extends_list: /* empty */ { $$ = NNEW(n_EMPTY); } | T_EXTENDS interface_list { NTYPE($1, n_EXTENDS_LIST); $1->appendChildren($2); $$ = $1; } ; implements_list: /* empty */ { $$ = NNEW(n_EMPTY); } | T_IMPLEMENTS interface_list { NTYPE($1, n_IMPLEMENTS_LIST); $1->appendChildren($2); $$ = $1; } ; interface_list: fully_qualified_class_name { $$ = NNEW(n_IMPLEMENTS_LIST)->appendChild($1); } | interface_list ',' fully_qualified_class_name { $$ = $1->appendChild($3); } ; foreach_optional_arg: /* empty */ { $$ = NNEW(n_EMPTY); } | T_DOUBLE_ARROW foreach_variable { $$ = $2; } ; foreach_variable: variable | '&' variable { NTYPE($1, n_VARIABLE_REFERENCE); $1->appendChild($2); $$ = $1; } ; for_statement: statement | ':' inner_statement_list T_ENDFOR ';' { NLMORE($2, $1); NMORE($2, $4); $$ = $2; } ; foreach_statement: statement | ':' inner_statement_list T_ENDFOREACH ';' { NLMORE($2, $1); NMORE($2, $4); $$ = $2; } ; declare_statement: statement | ':' inner_statement_list T_ENDDECLARE ';' { NLMORE($2, $1); NMORE($2, $4); $$ = $2; } ; declare_list: T_STRING '=' static_scalar { $$ = NNEW(n_DECLARE_DECLARATION); $$->appendChild(NTYPE($1, n_STRING)); $$->appendChild($3); $$ = NNEW(n_DECLARE_DECLARATION_LIST)->appendChild($$); } | declare_list ',' T_STRING '=' static_scalar { $$ = NNEW(n_DECLARE_DECLARATION); $$->appendChild(NTYPE($3, n_STRING)); $$->appendChild($5); $1->appendChild($$); $$ = $1; } ; switch_case_list: '{' case_list '}' { $$ = NEXPAND($1, $2, $3); } | '{' ';' case_list '}' { // ...why does this rule exist? NTYPE($2, n_STATEMENT); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT_LIST)->appendChild($2); $$->appendChildren($3); NEXPAND($1, $$, $4); } | ':' case_list T_ENDSWITCH ';' { NMORE($2, $4); NLMORE($2, $1); $$ = $2; } | ':' ';' case_list T_ENDSWITCH ';' { NTYPE($2, n_STATEMENT); $1->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATEMENT_LIST)->appendChild($2); $$->appendChildren($3); NMORE($$, $5); NLMORE($$, $1); } ; case_list: /* empty */ { $$ = NNEW(n_STATEMENT_LIST); } | case_list T_CASE expr case_separator inner_statement_list { NTYPE($2, n_CASE); $2->appendChild($3); $2->appendChild($5); $1->appendChild($2); $$ = $1; } | case_list T_DEFAULT case_separator inner_statement_list { NTYPE($2, n_DEFAULT); $2->appendChild($4); $1->appendChild($2); $$ = $1; } ; case_separator: ':' | ';' ; while_statement: statement | ':' inner_statement_list T_ENDWHILE ';' { NMORE($2, $4); NLMORE($2, $1); $$ = $2; } ; elseif_list: /* empty */ { $$ = NNEW(n_CONDITION_LIST); } | elseif_list T_ELSEIF '(' expr ')' statement { NTYPE($2, n_ELSEIF); $2->appendChild(NSPAN($3, n_CONTROL_CONDITION, $5)->appendChild($4)); $2->appendChild($6); $$ = $1->appendChild($2); } ; new_elseif_list: /* empty */ { $$ = NNEW(n_CONDITION_LIST); } | new_elseif_list T_ELSEIF '(' expr ')' ':' inner_statement_list { NTYPE($2, n_ELSEIF); $2->appendChild($4); $2->appendChild($7); $$ = $1->appendChild($2); } ; else_single: /* empty */ { $$ = NNEW(n_EMPTY); } | T_ELSE statement { NTYPE($1, n_ELSE); $1->appendChild($2); $$ = $1; } ; new_else_single: /* empty */ { $$ = NNEW(n_EMPTY); } | T_ELSE ':' inner_statement_list { NTYPE($1, n_ELSE); $1->appendChild($3); $$ = $1; } ; parameter_list: non_empty_parameter_list | /* empty */ { $$ = NNEW(n_DECLARATION_PARAMETER_LIST); } ; non_empty_parameter_list: optional_class_type T_VARIABLE { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($1); $$->appendChild(NTYPE($2, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); } | optional_class_type '&' T_VARIABLE { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($1); $$->appendChild(NTYPE($2, n_VARIABLE_REFERENCE)); $2->appendChild(NTYPE($3, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); } | optional_class_type '&' T_VARIABLE '=' static_scalar { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($1); $$->appendChild(NTYPE($2, n_VARIABLE_REFERENCE)); $2->appendChild(NTYPE($3, n_VARIABLE)); $$->appendChild($5); $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); } | optional_class_type T_VARIABLE '=' static_scalar { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($1); $$->appendChild(NTYPE($2, n_VARIABLE)); $$->appendChild($4); $$ = NNEW(n_DECLARATION_PARAMETER_LIST)->appendChild($$); } | non_empty_parameter_list ',' optional_class_type T_VARIABLE { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($3); $$->appendChild(NTYPE($4, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = $1->appendChild($$); } | non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($3); $$->appendChild(NTYPE($4, n_VARIABLE_REFERENCE)); $4->appendChild(NTYPE($5, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = $1->appendChild($$); } | non_empty_parameter_list ',' optional_class_type '&' T_VARIABLE '=' static_scalar { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($3); $$->appendChild(NTYPE($4, n_VARIABLE_REFERENCE)); $4->appendChild(NTYPE($5, n_VARIABLE)); $$->appendChild($7); $$ = $1->appendChild($$); } | non_empty_parameter_list ',' optional_class_type T_VARIABLE '=' static_scalar { $$ = NNEW(n_DECLARATION_PARAMETER); $$->appendChild($3); $$->appendChild(NTYPE($4, n_VARIABLE)); $$->appendChild($6); $$ = $1->appendChild($$); } ; optional_class_type: /* empty */ { $$ = NNEW(n_EMPTY); } | fully_qualified_class_name { $$ = $1; } | T_ARRAY { $$ = NTYPE($1, n_TYPE_NAME); } ; function_call_parameter_list: non_empty_function_call_parameter_list | /* empty */ { $$ = NNEW(n_CALL_PARAMETER_LIST); } ; non_empty_function_call_parameter_list: expr_without_variable { $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); } | variable { $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); } | '&' w_variable { NTYPE($1, n_VARIABLE_REFERENCE); $1->appendChild($2); $$ = NNEW(n_CALL_PARAMETER_LIST)->appendChild($1); } | non_empty_function_call_parameter_list ',' expr_without_variable { $$ = $1->appendChild($3); } | non_empty_function_call_parameter_list ',' variable { $$ = $1->appendChild($3); } | non_empty_function_call_parameter_list ',' '&' w_variable { $$ = $1->appendChild($3); } ; global_var_list: global_var_list ',' global_var { $1->appendChild($3); $$ = $1; } | global_var { $$ = NNEW(n_GLOBAL_DECLARATION_LIST); $$->appendChild($1); } ; global_var: T_VARIABLE { $$ = NTYPE($1, n_VARIABLE); } | '$' r_variable { $$ = NTYPE($1, n_VARIABLE_VARIABLE); $$->appendChild($2); } | '$' '{' expr '}' { $$ = NTYPE($1, n_VARIABLE_VARIABLE); $$->appendChild($3); } ; static_var_list: static_var_list ',' T_VARIABLE { NTYPE($3, n_VARIABLE); $$ = NNEW(n_STATIC_DECLARATION); $$->appendChild($3); $$->appendChild(NNEW(n_EMPTY)); $$ = $1->appendChild($$); } | static_var_list ',' T_VARIABLE '=' static_scalar { NTYPE($3, n_VARIABLE); $$ = NNEW(n_STATIC_DECLARATION); $$->appendChild($3); $$->appendChild($5); $$ = $1->appendChild($$); } | T_VARIABLE { NTYPE($1, n_VARIABLE); $$ = NNEW(n_STATIC_DECLARATION); $$->appendChild($1); $$->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_STATIC_DECLARATION_LIST)->appendChild($$); } | T_VARIABLE '=' static_scalar { NTYPE($1, n_VARIABLE); $$ = NNEW(n_STATIC_DECLARATION); $$->appendChild($1); $$->appendChild($3); $$ = NNEW(n_STATIC_DECLARATION_LIST)->appendChild($$); } ; class_statement_list: class_statement_list class_statement { $$ = $1->appendChild($2); } | /* empty */ { $$ = NNEW(n_STATEMENT_LIST); } ; class_statement: variable_modifiers class_variable_declaration ';' { $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST); $$->appendChild($1); $$->appendChildren($2); $$ = NNEW(n_STATEMENT)->appendChild($$); NMORE($$, $3); } | class_constant_declaration ';' { $$ = NNEW(n_STATEMENT)->appendChild($1); NMORE($$, $2); } | method_modifiers function { yyextra->old_expecting_xhp_class_statements = yyextra->expecting_xhp_class_statements; yyextra->expecting_xhp_class_statements = false; } is_reference T_STRING '(' parameter_list ')' method_body { yyextra->expecting_xhp_class_statements = yyextra->old_expecting_xhp_class_statements; $$ = NNEW(n_METHOD_DECLARATION); $$->appendChild($1); $$->appendChild($4); $$->appendChild(NTYPE($5, n_STRING)); $$->appendChild(NEXPAND($6, $7, $8)); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($9); $$ = NNEW(n_STATEMENT)->appendChild($$); } ; method_body: ';' /* abstract method */ { $$ = NNEW(n_EMPTY); } | '{' inner_statement_list '}' { $$ = NEXPAND($1, $2, $3); } ; variable_modifiers: non_empty_member_modifiers | T_VAR { $$ = NNEW(n_CLASS_MEMBER_MODIFIER_LIST); $$->appendChild(NTYPE($1, n_STRING)); } ; method_modifiers: /* empty */ { $$ = NNEW(n_METHOD_MODIFIER_LIST); } | non_empty_member_modifiers { NTYPE($1, n_METHOD_MODIFIER_LIST); $$ = $1; } ; non_empty_member_modifiers: member_modifier { $$ = NNEW(n_CLASS_MEMBER_MODIFIER_LIST); $$->appendChild(NTYPE($1, n_STRING)); } | non_empty_member_modifiers member_modifier { $$ = $1->appendChild(NTYPE($2, n_STRING)); } ; member_modifier: T_PUBLIC | T_PROTECTED | T_PRIVATE | T_STATIC | T_ABSTRACT | T_FINAL ; class_variable_declaration: class_variable_declaration ',' T_VARIABLE { $$ = NNEW(n_CLASS_MEMBER_DECLARATION); $$->appendChild(NTYPE($3, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = $1->appendChild($$); } | class_variable_declaration ',' T_VARIABLE '=' static_scalar { $$ = NNEW(n_CLASS_MEMBER_DECLARATION); $$->appendChild(NTYPE($3, n_VARIABLE)); $$->appendChild($5); $$ = $1->appendChild($$); } | T_VARIABLE { $$ = NNEW(n_CLASS_MEMBER_DECLARATION); $$->appendChild(NTYPE($1, n_VARIABLE)); $$->appendChild(NNEW(n_EMPTY)); $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST)->appendChild($$); } | T_VARIABLE '=' static_scalar { $$ = NNEW(n_CLASS_MEMBER_DECLARATION); $$->appendChild(NTYPE($1, n_VARIABLE)); $$->appendChild($3); $$ = NNEW(n_CLASS_MEMBER_DECLARATION_LIST)->appendChild($$); } ; class_constant_declaration: class_constant_declaration ',' T_STRING '=' static_scalar { $$ = NNEW(n_CLASS_CONSTANT_DECLARATION); $$->appendChild(NTYPE($3, n_STRING)); $$->appendChild($5); $1->appendChild($$); $$ = $1; } | T_CONST T_STRING '=' static_scalar { NTYPE($1, n_CLASS_CONSTANT_DECLARATION_LIST); $$ = NNEW(n_CLASS_CONSTANT_DECLARATION); $$->appendChild(NTYPE($2, n_STRING)); $$->appendChild($4); $1->appendChild($$); $$ = $1; } ; echo_expr_list: echo_expr_list ',' expr { $1->appendChild($3); } | expr { $$ = NNEW(n_ECHO_LIST); $$->appendChild($1); } ; for_expr: /* empty */ { $$ = NNEW(n_EMPTY); } | non_empty_for_expr ; non_empty_for_expr: non_empty_for_expr ',' expr { $1->appendChild($3); } | expr { $$ = NNEW(n_EXPRESSION_LIST); $$->appendChild($1); } ; expr_without_variable: T_LIST '(' assignment_list ')' '=' expr { NTYPE($1, n_LIST); $1->appendChild(NEXPAND($2, $3, $4)); $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($5, n_OPERATOR)); $$->appendChild($6); } | variable '=' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable '=' '&' variable { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); NTYPE($3, n_VARIABLE_REFERENCE); $3->appendChild($4); $$->appendChild($3); } | variable '=' '&' T_NEW class_name_reference ctor_arguments { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); NTYPE($4, n_NEW); $4->appendChild($5); $4->appendChild($6); NTYPE($3, n_VARIABLE_REFERENCE); $3->appendChild($4); $$->appendChild($3); } | T_NEW class_name_reference ctor_arguments { NTYPE($1, n_NEW); $1->appendChild($2); $1->appendChild($3); $$ = $1; } | T_CLONE expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | variable T_PLUS_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_MINUS_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_MUL_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_DIV_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_CONCAT_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_MOD_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_AND_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_OR_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_XOR_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_SL_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | variable T_SR_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | rw_variable T_INC { $$ = NNEW(n_UNARY_POSTFIX_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); } | T_INC rw_variable { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | rw_variable T_DEC { $$ = NNEW(n_UNARY_POSTFIX_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); } | T_DEC rw_variable { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | expr T_BOOLEAN_OR expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_BOOLEAN_AND expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_LOGICAL_OR expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_LOGICAL_AND expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_LOGICAL_XOR expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '|' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '&' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '^' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '.' expr { /* The concatenation operator generates n_CONCATENATION_LIST instead of n_BINARY_EXPRESSION because we tend to run into stack depth issues in a lot of real-world cases otherwise (e.g., in PHP and JSON decoders). */ if ($1->type == n_CONCATENATION_LIST && $3->type == n_CONCATENATION_LIST) { $1->appendChild(NTYPE($2, n_OPERATOR)); $1->appendChildren($3); $$ = $1; } else if ($1->type == n_CONCATENATION_LIST) { $1->appendChild(NTYPE($2, n_OPERATOR)); $1->appendChild($3); $$ = $1; } else if ($3->type == n_CONCATENATION_LIST) { $$ = NNEW(n_CONCATENATION_LIST); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChildren($3); } else { $$ = NNEW(n_CONCATENATION_LIST); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } } | expr '+' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '-' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '*' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '/' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '%' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_SL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_SR expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | '+' expr %prec T_INC { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | '-' expr %prec T_INC { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | '!' expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | '~' expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | expr T_IS_IDENTICAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_IS_NOT_IDENTICAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_IS_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_IS_NOT_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '<' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_IS_SMALLER_OR_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr '>' expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_IS_GREATER_OR_EQUAL expr { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | expr T_INSTANCEOF class_name_reference { $$ = NNEW(n_BINARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NTYPE($2, n_OPERATOR)); $$->appendChild($3); } | '(' expr ')' { NSPAN($1, n_PARENTHETICAL_EXPRESSION, $3); $1->appendChild($2); $$ = $1; } | expr '?' expr ':' expr { $$ = NNEW(n_TERNARY_EXPRESSION); $$->appendChild($1); $$->appendChild($3); $$->appendChild($5); } | expr '?' ':' expr { $$ = NNEW(n_TERNARY_EXPRESSION); $$->appendChild($1); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($4); } | internal_functions_in_yacc | T_INT_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_DOUBLE_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_STRING_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_UNICODE_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_BINARY_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_ARRAY_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_OBJECT_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_BOOL_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_UNSET_CAST expr { $$ = NNEW(n_CAST_EXPRESSION); $$->appendChild(NTYPE($1, n_CAST)); $$->appendChild($2); } | T_EXIT exit_expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | '@' expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | T_ARRAY '(' array_pair_list ')' { NTYPE($1, n_ARRAY_LITERAL); $1->appendChild($3); NMORE($1, $4); $$ = $1; } | T_BACKTICKS_EXPR { NTYPE($1, n_BACKTICKS_EXPRESSION); $$ = $1; } | scalar | T_PRINT expr { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | function is_reference '(' parameter_list ')' lexical_vars '{' inner_statement_list '}' { NSPAN($1, n_FUNCTION_DECLARATION, $9); $1->appendChild(NNEW(n_EMPTY)); $1->appendChild($2); $1->appendChild(NNEW(n_EMPTY)); $1->appendChild(NEXPAND($3, $4, $5)); $$->appendChild($6); $1->appendChild(NEXPAND($7, $8, $9)); $$ = $1; } | T_STATIC function is_reference '(' parameter_list ')' lexical_vars '{' inner_statement_list '}' { NSPAN($2, n_FUNCTION_DECLARATION, $10); NLMORE($2, $1); $$ = NNEW(n_FUNCTION_MODIFIER_LIST); $$->appendChild(NTYPE($1, n_STRING)); $2->appendChild($1); $2->appendChild(NNEW(n_EMPTY)); $2->appendChild($3); $2->appendChild(NNEW(n_EMPTY)); $2->appendChild(NEXPAND($4, $5, $6)); $2->appendChild($7); $2->appendChild(NEXPAND($8, $9, $10)); $$ = $2; } ; function: T_FUNCTION ; lexical_vars: /* empty */ { $$ = NNEW(n_EMPTY); } | T_USE '(' lexical_var_list ')' { NTYPE($1, n_LEXICAL_VARIABLE_LIST); $1->appendChildren($3); $$ = $1; } ; lexical_var_list: lexical_var_list ',' T_VARIABLE { $$ = $1->appendChild(NTYPE($3, n_VARIABLE)); } | lexical_var_list ',' '&' T_VARIABLE { NTYPE($3, n_VARIABLE_REFERENCE); $3->appendChild(NTYPE($4, n_VARIABLE)); $$ = $1->appendChild($3); } | T_VARIABLE { $$ = NNEW(n_LEXICAL_VARIABLE_LIST); $$->appendChild(NTYPE($1, n_VARIABLE)); } | '&' T_VARIABLE { NTYPE($1, n_VARIABLE_REFERENCE); $1->appendChild(NTYPE($2, n_VARIABLE)); $$ = NNEW(n_LEXICAL_VARIABLE_LIST); $$->appendChild($1); } ; function_call: namespace_name '(' function_call_parameter_list ')' { $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($1); $$->appendChild(NEXPAND($2, $3, $4)); } | T_NAMESPACE T_NS_SEPARATOR namespace_name '(' function_call_parameter_list ')' { NLMORE($3, $1); $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($3); $$->appendChild(NEXPAND($4, $5, $6)); } | T_NS_SEPARATOR namespace_name '(' function_call_parameter_list ')' { NLMORE($2, $1); $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($2); $$->appendChild(NEXPAND($3, $4, $5)); } | class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' function_call_parameter_list ')' { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); $$->appendChild(NEXPAND($4, $5, $6)); } | variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING '(' function_call_parameter_list ')' { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); $$->appendChild(NEXPAND($4, $5, $6)); } | variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' function_call_parameter_list ')' { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); $$->appendChild(NEXPAND($4, $5, $6)); } | class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects '(' function_call_parameter_list ')' { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); $$ = NNEW(n_FUNCTION_CALL)->appendChild($$); $$->appendChild(NEXPAND($4, $5, $6)); } | variable_without_objects '(' function_call_parameter_list ')' { $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($1); $$->appendChild(NEXPAND($2, $3, $4)); } ; class_name: T_STATIC { $$ = NTYPE($1, n_CLASS_NAME); } | namespace_name { $$ = NTYPE($1, n_CLASS_NAME); } | T_NAMESPACE T_NS_SEPARATOR namespace_name { NLMORE($3, $1); $$ = NTYPE($3, n_CLASS_NAME); } | T_NS_SEPARATOR namespace_name { NLMORE($2, $1); $$ = NTYPE($2, n_CLASS_NAME); } ; fully_qualified_class_name: namespace_name { $$ = NTYPE($1, n_CLASS_NAME); } | T_NAMESPACE T_NS_SEPARATOR namespace_name { NLMORE($3, $1); $$ = NTYPE($3, n_CLASS_NAME); } | T_NS_SEPARATOR namespace_name { NLMORE($2, $1); $$ = NTYPE($2, n_CLASS_NAME); } ; class_name_reference: class_name | dynamic_class_name_reference ; dynamic_class_name_reference: base_variable T_OBJECT_OPERATOR object_property dynamic_class_name_variable_properties { $$ = NNEW(n_OBJECT_PROPERTY_ACCESS); $$->appendChild($1); $$->appendChild($3); for (xhpast::node_list_t::iterator ii = $4->children.begin(); ii != $4->children.end(); ++ii) { $$ = NNEW(n_OBJECT_PROPERTY_ACCESS)->appendChild($$); $$->appendChild(*ii); } } | base_variable ; dynamic_class_name_variable_properties: dynamic_class_name_variable_properties dynamic_class_name_variable_property { $$ = $1->appendChild($2); } | /* empty */ { $$ = NNEW(n_EMPTY); } ; dynamic_class_name_variable_property: T_OBJECT_OPERATOR object_property { $$ = $2; } ; exit_expr: /* empty */ { $$ = NNEW(n_EMPTY); } | '(' ')' { NSPAN($1, n_EMPTY, $2); $$ = $1; } | '(' expr ')' { NSPAN($1, n_PARENTHETICAL_EXPRESSION, $3); $1->appendChild($2); $$ = $1; } ; ctor_arguments: /* empty */ { $$ = NNEW(n_EMPTY); } | '(' function_call_parameter_list ')' { $$ = NEXPAND($1, $2, $3); } ; common_scalar: T_LNUMBER { $$ = NTYPE($1, n_NUMERIC_SCALAR); } | T_DNUMBER { $$ = NTYPE($1, n_NUMERIC_SCALAR); } | T_CONSTANT_ENCAPSED_STRING { $$ = NTYPE($1, n_STRING_SCALAR); } | T_LINE { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_FILE { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_DIR { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_CLASS_C { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_METHOD_C { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_FUNC_C { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_NS_C { $$ = NTYPE($1, n_MAGIC_SCALAR); } | T_HEREDOC { $$ = NTYPE($1, n_HEREDOC); } ; static_scalar: /* compile-time evaluated scalars */ common_scalar | namespace_name | T_NAMESPACE T_NS_SEPARATOR namespace_name { NLMORE($3, $1); $$ = $3; } | T_NS_SEPARATOR namespace_name { NLMORE($2, $1); $$ = $2; } | '+' static_scalar { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | '-' static_scalar { $$ = NNEW(n_UNARY_PREFIX_EXPRESSION); $$->appendChild(NTYPE($1, n_OPERATOR)); $$->appendChild($2); } | T_ARRAY '(' static_array_pair_list ')' { NTYPE($1, n_ARRAY_LITERAL); $1->appendChild($3); NMORE($1, $4); $$ = $1; } | static_class_constant ; static_class_constant: class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); } ; scalar: T_STRING_VARNAME | class_constant | namespace_name | T_NAMESPACE T_NS_SEPARATOR namespace_name { $$ = NLMORE($3, $1); } | T_NS_SEPARATOR namespace_name { $$ = NLMORE($2, $1); } | common_scalar ; static_array_pair_list: /* empty */ { $$ = NNEW(n_ARRAY_VALUE_LIST); } | non_empty_static_array_pair_list possible_comma { $$ = NMORE($1, $2); } ; possible_comma: /* empty */ { $$ = NNEW(n_EMPTY); } | ',' ; non_empty_static_array_pair_list: non_empty_static_array_pair_list ',' static_scalar T_DOUBLE_ARROW static_scalar { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($3); $$->appendChild($5); $$ = $1->appendChild($$); } | non_empty_static_array_pair_list ',' static_scalar { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($3); $$ = $1->appendChild($$); } | static_scalar T_DOUBLE_ARROW static_scalar { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($1); $$->appendChild($3); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } | static_scalar { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($1); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } ; expr: r_variable | expr_without_variable ; r_variable: variable ; w_variable: variable ; rw_variable: variable ; variable: base_variable_with_function_calls T_OBJECT_OPERATOR object_property method_or_not variable_properties { $$ = NNEW(n_OBJECT_PROPERTY_ACCESS); $$->appendChild($1); $$->appendChild($3); if ($4->type != n_EMPTY) { $$ = NNEW(n_METHOD_CALL)->appendChild($$); $$->appendChild($4); } for (xhpast::node_list_t::iterator ii = $5->children.begin(); ii != $5->children.end(); ++ii) { if ((*ii)->type == n_CALL_PARAMETER_LIST) { $$ = NNEW(n_METHOD_CALL)->appendChild($$); $$->appendChild((*ii)); } else { $$ = NNEW(n_OBJECT_PROPERTY_ACCESS)->appendChild($$); $$->appendChild((*ii)); } } } | base_variable_with_function_calls ; variable_properties: variable_properties variable_property { $$ = $1->appendChildren($2); } | /* empty */ { $$ = NNEW(n_EMPTY); } ; variable_property: T_OBJECT_OPERATOR object_property method_or_not { $$ = NNEW(n_EMPTY); $$->appendChild($2); if ($3->type != n_EMPTY) { $$->appendChild($3); } } ; method_or_not: '(' function_call_parameter_list ')' { $$ = NEXPAND($1, $2, $3); } | /* empty */ { $$ = NNEW(n_EMPTY); } ; variable_without_objects: reference_variable | simple_indirect_reference reference_variable { xhpast::Node *last = $1; NMORE($1, $2); while (last->firstChild() && last->firstChild()->type == n_VARIABLE_VARIABLE) { NMORE(last, $2); last = last->firstChild(); } last->appendChild($2); $$ = $1; } ; static_member: class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild($3); } | variable_class_name T_PAAMAYIM_NEKUDOTAYIM variable_without_objects { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild($3); } ; variable_class_name: reference_variable ; base_variable_with_function_calls: base_variable | function_call ; base_variable: reference_variable | simple_indirect_reference reference_variable { xhpast::Node *last = $1; NMORE($1, $2); while (last->firstChild() && last->firstChild()->type == n_VARIABLE_VARIABLE) { NMORE(last, $2); last = last->firstChild(); } last->appendChild($2); $$ = $1; } | static_member ; reference_variable: reference_variable '[' dim_offset ']' { $$ = NNEW(n_INDEX_ACCESS); $$->appendChild($1); $$->appendChild($3); NMORE($$, $4); } | reference_variable '{' expr '}' { $$ = NNEW(n_INDEX_ACCESS); $$->appendChild($1); $$->appendChild($3); NMORE($$, $4); } | compound_variable ; compound_variable: T_VARIABLE { NTYPE($1, n_VARIABLE); } | '$' '{' expr '}' { NSPAN($1, n_VARIABLE_EXPRESSION, $4); $1->appendChild($3); $$ = $1; } ; dim_offset: /* empty */ { $$ = NNEW(n_EMPTY); } | expr { $$ = $1; } ; object_property: object_dim_list | variable_without_objects ; object_dim_list: object_dim_list '[' dim_offset ']' { $$ = NNEW(n_INDEX_ACCESS); $$->appendChild($1); $$->appendChild($3); NMORE($$, $4) } | object_dim_list '{' expr '}' { $$ = NNEW(n_INDEX_ACCESS); $$->appendChild($1); $$->appendChild($3); NMORE($$, $4); } | variable_name ; variable_name: T_STRING { NTYPE($1, n_STRING); $$ = $1; } | '{' expr '}' { $$ = NEXPAND($1, $2, $3); } ; simple_indirect_reference: '$' { $$ = NTYPE($1, n_VARIABLE_VARIABLE); } | simple_indirect_reference '$' { $2 = NTYPE($2, n_VARIABLE_VARIABLE); xhpast::Node *last = $1; while (last->firstChild() && last->firstChild()->type == n_VARIABLE_VARIABLE) { last = last->firstChild(); } last->appendChild($2); $$ = $1; } ; assignment_list: assignment_list ',' assignment_list_element { $$ = $1->appendChild($3); } | assignment_list_element { $$ = NNEW(n_ASSIGNMENT_LIST); $$->appendChild($1); } ; assignment_list_element: variable | T_LIST '(' assignment_list ')' { $$ = NNEW(n_LIST); $$->appendChild($3); NMORE($$, $4); } | /* empty */ { $$ = NNEW(n_EMPTY); } ; array_pair_list: /* empty */ { $$ = NNEW(n_ARRAY_VALUE_LIST); } | non_empty_array_pair_list possible_comma { $$ = NMORE($1, $2); } ; non_empty_array_pair_list: non_empty_array_pair_list ',' expr T_DOUBLE_ARROW expr { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($3); $$->appendChild($5); $$ = $1->appendChild($$); } | non_empty_array_pair_list ',' expr { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($3); $$ = $1->appendChild($$); } | expr T_DOUBLE_ARROW expr { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($1); $$->appendChild($3); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } | expr { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild($1); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } | non_empty_array_pair_list ',' expr T_DOUBLE_ARROW '&' w_variable { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($3); $$->appendChild(NTYPE($5, n_VARIABLE_REFERENCE)->appendChild($6)); $$ = $1->appendChild($$); } | non_empty_array_pair_list ',' '&' w_variable { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild(NTYPE($3, n_VARIABLE_REFERENCE)->appendChild($4)); $$ = $1->appendChild($$); } | expr T_DOUBLE_ARROW '&' w_variable { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild($1); $$->appendChild(NTYPE($3, n_VARIABLE_REFERENCE)->appendChild($4)); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } | '&' w_variable { $$ = NNEW(n_ARRAY_VALUE); $$->appendChild(NNEW(n_EMPTY)); $$->appendChild(NTYPE($1, n_VARIABLE_REFERENCE)->appendChild($2)); $$ = NNEW(n_ARRAY_VALUE_LIST)->appendChild($$); } ; internal_functions_in_yacc: T_ISSET '(' isset_variables ')' { NTYPE($1, n_SYMBOL_NAME); NSPAN($2, n_CALL_PARAMETER_LIST, $4); $2->appendChildren($3); $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($1); $$->appendChild($2); } | T_EMPTY '(' variable ')' { NTYPE($1, n_SYMBOL_NAME); NSPAN($2, n_CALL_PARAMETER_LIST, $4); $2->appendChild($3); $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($1); $$->appendChild($2); } | T_INCLUDE expr { $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); } | T_INCLUDE_ONCE expr { $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); } | T_EVAL '(' expr ')' { NTYPE($1, n_SYMBOL_NAME); NSPAN($2, n_CALL_PARAMETER_LIST, $4); $2->appendChild($3); $$ = NNEW(n_FUNCTION_CALL); $$->appendChild($1); $$->appendChild($2); } | T_REQUIRE expr { $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); } | T_REQUIRE_ONCE expr { $$ = NTYPE($1, n_INCLUDE_FILE)->appendChild($2); } ; isset_variables: variable { $$ = NNEW(n_EMPTY); $$->appendChild($1); } | isset_variables ',' variable { $$ = $1->appendChild($3); } ; class_constant: class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); } | variable_class_name T_PAAMAYIM_NEKUDOTAYIM T_STRING { $$ = NNEW(n_CLASS_STATIC_ACCESS); $$->appendChild($1); $$->appendChild(NTYPE($3, n_STRING)); } ; // Fix the "bug" in PHP's grammar where you can't chain the [] operator on a // function call. // This introduces some shift/reduce conflicts. We want the shift here to fall // back to regular PHP grammar. In the case where it's an extension of the PHP // grammar our code gets picked up. expr_without_variable: expr '[' dim_offset ']' { if (yyextra->idx_expr) { yyextra->used = true; } $$ = NNEW(n_INDEX_ACCESS); $$->appendChild($1); $$->appendChild($3); NMORE($$, $4); } ; %% const char* yytokname(int tok) { if (tok < 255) { return NULL; } return yytname[YYTRANSLATE(tok)]; } diff --git a/support/xhpast/scanner.l b/support/xhpast/scanner.l index 8bc8417..6825930 100644 --- a/support/xhpast/scanner.l +++ b/support/xhpast/scanner.l @@ -1,538 +1,522 @@ -/* - * Copyright 2012 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. - */ - %{ #include "ast.hpp" #define push_state(s) xhp_new_push_state(s, yyg) #define pop_state() xhp_new_pop_state(yyg) #define set_state(s) xhp_set_state(s, yyg) #define last_token() yyextra->last_token #define YY_USER_ACTION \ if (!yyg->yy_more_len) \ yyextra->first_lineno = yyextra->lineno; #define pttok(t, txt) \ yyextra->token_list.push_back(new xhpast::Token(t, txt, yyextra->list_size++)); \ *yylval = new xhpast::Node(0, yyextra->list_size - 1); #define ptok(t) \ pttok(t, yytext); #define tok(t) \ ptok(t); \ return yy_token(t, yyg) #define YY_USER_INIT \ if (yyextra->insert_token) { \ yyg->yy_init = 0; \ int ft = yyextra->insert_token; \ yyextra->insert_token = 0; \ return yy_token(ft, yyg); \ } using namespace std; const char* yytokname(int tok); static int yy_token(int tok, struct yyguts_t* yyg); static void yy_scan_newlines(const char* text, struct yyguts_t* yyg); %} %option prefix="xhpast" %option reentrant /* PHP allows IF or if */ %option case-insensitive %option noyywrap nodefault %option stack %option bison-bridge %option 8bit /* I think an interactive scanner is required because of the bison state * pushing we do. I'm putting an explicit interactive declaration here in case * someone tries adding -CF or whatever to the make flags. */ %option interactive /* The different lexing states. Note that the transitions are done either * in the lex actions, or in a generic manner in yy_token(). */ %s PHP %s PHP_COMMENT %s PHP_EOL_COMMENT %s PHP_DOC_COMMENT %s PHP_HEREDOC_START %s PHP_HEREDOC_NSTART %s PHP_HEREDOC_NEWLINE %s PHP_HEREDOC_DATA %s PHP_NO_RESERVED_WORDS %s PHP_NO_RESERVED_WORDS_PERSIST %s PHP_ LNUM [0-9]+ DNUM ([0-9]*"."[0-9]+)|([0-9]+"."[0-9]*) EXPONENT_DNUM (({LNUM}|{DNUM})[eE][+-]?{LNUM}) HNUM "0x"[0-9a-fA-F]+ LABEL [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* BYTE (.|\n) WHITESPACE [ \n\r\t]+ TABS_AND_SPACES [ \t]* NEWLINE ("\r\n"|"\n"|"\r") %% /* Open / close PHP + inline HTML */ { "short_tags) { tok(T_OPEN_TAG); } else { tok(T_INLINE_HTML); } } "short_tags) { tok(T_OPEN_TAG_WITH_ECHO); } else { tok(T_INLINE_HTML); } } "<%" { if (yyextra->asp_tags) { tok(T_OPEN_TAG); } else { tok(T_INLINE_HTML); } } "<%=" { if (yyextra->asp_tags) { tok(T_OPEN_TAG_WITH_ECHO); } else { tok(T_INLINE_HTML); } } "<"|[^<]* { yy_scan_newlines(yytext, yyg); tok(T_INLINE_HTML); } } { ("?>"|""){NEWLINE}? { yy_scan_newlines(yytext + 2, yyg); tok(T_CLOSE_TAG); } "%>" { if (yyextra->asp_tags) { tok(T_CLOSE_TAG); } else { yyless(1); tok(yytext[0]); } } } /* Comments and whitespace */ { "#"|"//" { push_state(PHP_EOL_COMMENT); yymore(); } "/**"{WHITESPACE} { yy_scan_newlines(yytext + 3, yyg); push_state(PHP_DOC_COMMENT); yymore(); } "/*" { push_state(PHP_COMMENT); yymore(); } {WHITESPACE}+ { yy_scan_newlines(yytext, yyg); ptok(T_WHITESPACE); } } <> { ptok(T_COMMENT); pop_state(); } { {NEWLINE} { ++yyextra->lineno; ptok(T_COMMENT); pop_state(); } [^\r\n?]+ yymore(); "?>" { yyless(yyleng - 2); ptok(T_COMMENT); pop_state(); } . yymore(); } { {NEWLINE} { ++yyextra->lineno; yymore(); } [^*\r\n]+|"*" yymore(); } "*/" { ptok(T_DOC_COMMENT); pop_state(); } <> { ptok(T_DOC_COMMENT); pop_state(); } "*/" { ptok(T_COMMENT); pop_state(); } <> { ptok(T_COMMENT); pop_state(); } /* Reserved words */ { include tok(T_INCLUDE); include_once tok(T_INCLUDE_ONCE); eval tok(T_EVAL); require tok(T_REQUIRE); require_once tok(T_REQUIRE_ONCE); or tok(T_LOGICAL_OR); xor tok(T_LOGICAL_XOR); and tok(T_LOGICAL_AND); print tok(T_PRINT); instanceof tok(T_INSTANCEOF); new tok(T_NEW); clone tok(T_CLONE); exit tok(T_EXIT); if tok(T_IF); elseif tok(T_ELSEIF); else tok(T_ELSE); endif tok(T_ENDIF); echo tok(T_ECHO); do tok(T_DO); while tok(T_WHILE); endwhile tok(T_ENDWHILE); for tok(T_FOR); endfor tok(T_ENDFOR); foreach tok(T_FOREACH); endforeach tok(T_ENDFOREACH); declare tok(T_DECLARE); enddeclare tok(T_ENDDECLARE); as tok(T_AS); switch tok(T_SWITCH); endswitch tok(T_ENDSWITCH); case tok(T_CASE); default tok(T_DEFAULT); break tok(T_BREAK); continue tok(T_CONTINUE); goto tok(T_GOTO); function tok(T_FUNCTION); const tok(T_CONST); return tok(T_RETURN); try tok(T_TRY); catch tok(T_CATCH); throw tok(T_THROW); use tok(T_USE); global tok(T_GLOBAL); static tok(T_STATIC); abstract tok(T_ABSTRACT); final tok(T_FINAL); private tok(T_PRIVATE); protected tok(T_PROTECTED); public tok(T_PUBLIC); var tok(T_VAR); unset tok(T_UNSET); isset tok(T_ISSET); empty tok(T_EMPTY); __halt_compiler tok(T_HALT_COMPILER); class tok(T_CLASS); interface tok(T_INTERFACE); extends tok(T_EXTENDS); implements tok(T_IMPLEMENTS); list tok(T_LIST); array tok(T_ARRAY); __class__ tok(T_CLASS_C); __method__ tok(T_METHOD_C); __function__ tok(T_FUNC_C); __line__ tok(T_LINE); __file__ tok(T_FILE); namespace tok(T_NAMESPACE); __namespace__ tok(T_NS_C); __dir__ tok(T_DIR); } /* Operators */ { "+=" tok(T_PLUS_EQUAL); "-=" tok(T_MINUS_EQUAL); "*=" tok(T_MUL_EQUAL); "/=" tok(T_DIV_EQUAL); ".=" tok(T_CONCAT_EQUAL); "%=" tok(T_MOD_EQUAL); "&=" tok(T_AND_EQUAL); "|=" tok(T_OR_EQUAL); "^=" tok(T_XOR_EQUAL); "<<=" tok(T_SL_EQUAL); ">>=" tok(T_SR_EQUAL); "||" tok(T_BOOLEAN_OR); "&&" tok(T_BOOLEAN_AND); "==" tok(T_IS_EQUAL); "!="|"<>" tok(T_IS_NOT_EQUAL); "===" tok(T_IS_IDENTICAL); "!==" tok(T_IS_NOT_IDENTICAL); "<=" tok(T_IS_SMALLER_OR_EQUAL); ">=" tok(T_IS_GREATER_OR_EQUAL); "<<" tok(T_SL); ">>" tok(T_SR); "++" tok(T_INC); "--" tok(T_DEC); "->" tok(T_OBJECT_OPERATOR); "=>" tok(T_DOUBLE_ARROW); "::" tok(T_PAAMAYIM_NEKUDOTAYIM); "\\" tok(T_NS_SEPARATOR); } /* Casts */ { "("{TABS_AND_SPACES}(int|integer){TABS_AND_SPACES}")" tok(T_INT_CAST); "("{TABS_AND_SPACES}(real|double|float){TABS_AND_SPACES}")" tok(T_DOUBLE_CAST); "("{TABS_AND_SPACES}string{TABS_AND_SPACES}")" tok(T_STRING_CAST); "("{TABS_AND_SPACES}unicode{TABS_AND_SPACES}")" tok(T_UNICODE_CAST); "("{TABS_AND_SPACES}binary{TABS_AND_SPACES}")" tok(T_BINARY_CAST); "("{TABS_AND_SPACES}array{TABS_AND_SPACES}")" tok(T_ARRAY_CAST); "("{TABS_AND_SPACES}object{TABS_AND_SPACES}")" tok(T_OBJECT_CAST); "("{TABS_AND_SPACES}(bool|boolean){TABS_AND_SPACES}")" tok(T_BOOL_CAST); "("{TABS_AND_SPACES}unset{TABS_AND_SPACES}")" tok(T_UNSET_CAST); } /* Scalars (parsing these doesn't really matter since we just pass them through literally) */ { {LNUM}|{HNUM} tok(T_LNUMBER); {DNUM}|{EXPONENT_DNUM} tok(T_DNUMBER); {LABEL} tok(T_STRING); "$"{LABEL} tok(T_VARIABLE); b?'(\\.|\\\n|[^\\']+)*'|b?\"(\\.|\\\n|[^\\\"]+)*\" { yy_scan_newlines(yytext, yyg); tok(T_CONSTANT_ENCAPSED_STRING); } `[^`]*` { yy_scan_newlines(yytext, yyg); tok(T_BACKTICKS_EXPR); } } /* (HERE|NOW)DOC's */ b?"<<<"{TABS_AND_SPACES} { push_state(PHP_HEREDOC_START); yyextra->heredoc_yyleng = yyleng; yymore(); } { "'"{LABEL}"'"|\"{LABEL}\" { // Create a new string for the heredoc label. Since we're using yymore above // yytext will actually start at the "<<<" and not the label. Use of // heredoc_yyleng jumps past that. Then we add 1 to get past the " or '. The // match is similar to calculate length. yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng + 1, yyleng - yyextra->heredoc_yyleng - 2); set_state(PHP_HEREDOC_NSTART); yyextra->heredoc_yyleng = yyleng; yymore(); } {LABEL} { yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng); set_state(PHP_HEREDOC_NSTART); yyextra->heredoc_yyleng = yyleng; yymore(); } } {NEWLINE} { ++yyextra->lineno; yyextra->heredoc_data = yytext + yyleng; set_state(PHP_HEREDOC_DATA); yymore(); } { [^\r\n]*{NEWLINE} { ++yyextra->lineno; set_state(PHP_HEREDOC_NEWLINE); yyextra->heredoc_yyleng = yyleng; yymore(); } } { {LABEL};?{NEWLINE} { if (strncmp(yyextra->heredoc_label.c_str(), yytext + yyextra->heredoc_yyleng, yyextra->heredoc_label.size()) == 0) { switch (yytext[yyextra->heredoc_yyleng + yyextra->heredoc_label.size()]) { case ';': case '\n': case '\r': yyless(yyleng - (yyleng - yyextra->heredoc_yyleng - yyextra->heredoc_label.size())); pop_state(); tok(T_HEREDOC); } } ++yyextra->lineno; yyextra->heredoc_yyleng = yyleng; yymore(); } [^\r\n]+ { set_state(PHP_HEREDOC_DATA); yyextra->heredoc_yyleng = yyleng; yymore(); } {NEWLINE} { ++yyextra->lineno; yyextra->heredoc_yyleng = yyleng; yymore(); } } /* Other */ <*>{BYTE} { tok(yytext[0]); // fix unused function warnings yy_top_state(NULL); yyunput(0, 0, NULL); } %% #ifdef DEBUG static const char* yy_state_name(int state) { switch (state) { case INITIAL: return "INITIAL"; case PHP: return "PHP"; case PHP_COMMENT: return "PHP_COMMENT"; case PHP_EOL_COMMENT: return "PHP_EOL_COMMENT"; case PHP_DOC_COMMENT: return "PHP_DOC_COMMENT"; case PHP_HEREDOC_START: return "PHP_HEREDOC_START"; case PHP_HEREDOC_NSTART: return "PHP_HEREDOC_NSTART"; case PHP_HEREDOC_NEWLINE: return "PHP_HEREDOC_NEWLINE"; case PHP_HEREDOC_DATA: return "PHP_HEREDOC_DATA"; case PHP_NO_RESERVED_WORDS: return "PHP_NO_RESERVED_WORDS"; case PHP_NO_RESERVED_WORDS_PERSIST: return "PHP_NO_RESERVED_WORDS_PERSIST"; default: return "???"; } } static void yy_log_token(int tok) { const char* tokname = yytokname(tok); if (tokname) { fprintf(stderr, "--> %s\n", tokname); } else { fprintf(stderr, "--> '%c'\n", tok); } } #endif static int yy_token(int tok, yyguts_t* yyg) { if (YY_START == PHP_NO_RESERVED_WORDS) { pop_state(); } switch (tok) { case T_OPEN_TAG: case T_OPEN_TAG_WITH_ECHO: case T_OPEN_TAG_FAKE: push_state(PHP); break; case T_CLOSE_TAG: pop_state(); // We need to return a ';', not a T_CLOSE_TAG, because a construct like // "" is valid and there are about a billion parser rules // which terminate with ';' so making a new rule like // "semicolon_or_close_tag" would be hard. The token in yylval has the // correct type and value, we just don't generate a node. return ';'; // In PHP it's ok to use keywords such as 'if' as field names // or function names. case T_OBJECT_OPERATOR: case T_FUNCTION: push_state(PHP_NO_RESERVED_WORDS); break; case T_PAAMAYIM_NEKUDOTAYIM: if (yyextra->colon_hack) { yyextra->colon_hack = false; } else { push_state(PHP_NO_RESERVED_WORDS); } break; case '{': // not used anymore yyextra->curly_stack.push(tok); break; } #ifdef DEBUG yy_log_token(tok); #endif return yyextra->last_token = tok; } static inline void yy_scan_newlines(const char* text, struct yyguts_t* yyg) { for (; *text; ++text) { if (*text == '\r') { if (text[1] == '\n') { ++text; } ++yyextra->lineno; } else if (*text == '\n') { ++yyextra->lineno; } } } void xhp_new_push_state(int s, struct yyguts_t* yyg) { #ifdef DEBUG fprintf(stderr, "--> PUSH(%s -> %s)\n", yy_state_name(YY_START), yy_state_name(s)); #endif yy_push_state(s, yyg); } void xhp_new_pop_state(struct yyguts_t* yyg) { #ifdef DEBUG int s = YY_START; #endif yy_pop_state(yyg); #ifdef DEBUG fprintf(stderr, "--> POP(%s -> %s)\n", yy_state_name(s), yy_state_name(YY_START)); #endif } void xhp_set_state(int s, struct yyguts_t* yyg) { #ifdef DEBUG fprintf(stderr, "--> SET(%s)\n", yy_state_name(s)); #endif BEGIN(s); } diff --git a/support/xhpast/scanner.lex.cpp b/support/xhpast/scanner.lex.cpp index 46c2866..9906f2c 100644 --- a/support/xhpast/scanner.lex.cpp +++ b/support/xhpast/scanner.lex.cpp @@ -1,6528 +1,6514 @@ #line 2 "scanner.lex.cpp" #line 4 "scanner.lex.cpp" #define YY_INT_ALIGNED short int /* A lexical scanner generated by flex */ /* %not-for-header */ /* %if-c-only */ /* %if-not-reentrant */ /* %endif */ /* %endif */ /* %ok-for-header */ #define FLEX_SCANNER #define YY_FLEX_MAJOR_VERSION 2 #define YY_FLEX_MINOR_VERSION 5 #define YY_FLEX_SUBMINOR_VERSION 35 #if YY_FLEX_SUBMINOR_VERSION > 0 #define FLEX_BETA #endif /* %if-c++-only */ /* %endif */ /* %if-c-only */ /* %endif */ /* %if-c-only */ /* %endif */ /* First, we deal with platform-specific or compiler-specific issues. */ /* begin standard C headers. */ /* %if-c-only */ #include #include #include #include /* %endif */ /* %if-tables-serialization */ /* %endif */ /* end standard C headers. */ /* %if-c-or-c++ */ /* flex integer type definitions */ #ifndef FLEXINT_H #define FLEXINT_H /* C99 systems have . Non-C99 systems may or may not. */ #if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 says to define __STDC_LIMIT_MACROS before including stdint.h, * if you want the limit (max/min) macros for int types. */ #ifndef __STDC_LIMIT_MACROS #define __STDC_LIMIT_MACROS 1 #endif #include typedef int8_t flex_int8_t; typedef uint8_t flex_uint8_t; typedef int16_t flex_int16_t; typedef uint16_t flex_uint16_t; typedef int32_t flex_int32_t; typedef uint32_t flex_uint32_t; #else typedef signed char flex_int8_t; typedef short int flex_int16_t; typedef int flex_int32_t; typedef unsigned char flex_uint8_t; typedef unsigned short int flex_uint16_t; typedef unsigned int flex_uint32_t; #endif /* ! C99 */ /* Limits of integral types. */ #ifndef INT8_MIN #define INT8_MIN (-128) #endif #ifndef INT16_MIN #define INT16_MIN (-32767-1) #endif #ifndef INT32_MIN #define INT32_MIN (-2147483647-1) #endif #ifndef INT8_MAX #define INT8_MAX (127) #endif #ifndef INT16_MAX #define INT16_MAX (32767) #endif #ifndef INT32_MAX #define INT32_MAX (2147483647) #endif #ifndef UINT8_MAX #define UINT8_MAX (255U) #endif #ifndef UINT16_MAX #define UINT16_MAX (65535U) #endif #ifndef UINT32_MAX #define UINT32_MAX (4294967295U) #endif #endif /* ! FLEXINT_H */ /* %endif */ /* %if-c++-only */ /* %endif */ #ifdef __cplusplus /* The "const" storage-class-modifier is valid. */ #define YY_USE_CONST #else /* ! __cplusplus */ /* C99 requires __STDC__ to be defined as 1. */ #if defined (__STDC__) #define YY_USE_CONST #endif /* defined (__STDC__) */ #endif /* ! __cplusplus */ #ifdef YY_USE_CONST #define yyconst const #else #define yyconst #endif /* %not-for-header */ /* Returned upon end-of-file. */ #define YY_NULL 0 /* %ok-for-header */ /* %not-for-header */ /* Promotes a possibly negative, possibly signed char to an unsigned * integer for use as an array index. If the signed char is negative, * we want to instead treat it as an 8-bit unsigned char, hence the * double cast. */ #define YY_SC_TO_UI(c) ((unsigned int) (unsigned char) c) /* %ok-for-header */ /* %if-reentrant */ /* An opaque pointer. */ #ifndef YY_TYPEDEF_YY_SCANNER_T #define YY_TYPEDEF_YY_SCANNER_T typedef void* yyscan_t; #endif /* For convenience, these vars (plus the bison vars far below) are macros in the reentrant scanner. */ #define yyin yyg->yyin_r #define yyout yyg->yyout_r #define yyextra yyg->yyextra_r #define yyleng yyg->yyleng_r #define yytext yyg->yytext_r #define yylineno (YY_CURRENT_BUFFER_LVALUE->yy_bs_lineno) #define yycolumn (YY_CURRENT_BUFFER_LVALUE->yy_bs_column) #define yy_flex_debug yyg->yy_flex_debug_r /* %endif */ /* %if-not-reentrant */ /* %endif */ /* Enter a start condition. This macro really ought to take a parameter, * but we do it the disgusting crufty way forced on us by the ()-less * definition of BEGIN. */ #define BEGIN yyg->yy_start = 1 + 2 * /* Translate the current start state into a value that can be later handed * to BEGIN to return to the state. The YYSTATE alias is for lex * compatibility. */ #define YY_START ((yyg->yy_start - 1) / 2) #define YYSTATE YY_START /* Action number for EOF rule of a given start state. */ #define YY_STATE_EOF(state) (YY_END_OF_BUFFER + state + 1) /* Special action meaning "start processing a new file". */ #define YY_NEW_FILE xhpastrestart(yyin ,yyscanner ) #define YY_END_OF_BUFFER_CHAR 0 /* Size of default input buffer. */ #ifndef YY_BUF_SIZE #define YY_BUF_SIZE 16384 #endif /* The state buf must be large enough to hold one state per character in the main buffer. */ #define YY_STATE_BUF_SIZE ((YY_BUF_SIZE + 2) * sizeof(yy_state_type)) #ifndef YY_TYPEDEF_YY_BUFFER_STATE #define YY_TYPEDEF_YY_BUFFER_STATE typedef struct yy_buffer_state *YY_BUFFER_STATE; #endif -#ifndef YY_TYPEDEF_YY_SIZE_T -#define YY_TYPEDEF_YY_SIZE_T -typedef size_t yy_size_t; -#endif - /* %if-not-reentrant */ /* %endif */ /* %if-c-only */ /* %if-not-reentrant */ /* %endif */ /* %endif */ #define EOB_ACT_CONTINUE_SCAN 0 #define EOB_ACT_END_OF_FILE 1 #define EOB_ACT_LAST_MATCH 2 #define YY_LESS_LINENO(n) /* Return all but the first "n" matched characters back to the input stream. */ #define yyless(n) \ do \ { \ /* Undo effects of setting up yytext. */ \ int yyless_macro_arg = (n); \ YY_LESS_LINENO(yyless_macro_arg);\ *yy_cp = yyg->yy_hold_char; \ YY_RESTORE_YY_MORE_OFFSET \ yyg->yy_c_buf_p = yy_cp = yy_bp + yyless_macro_arg - YY_MORE_ADJ; \ YY_DO_BEFORE_ACTION; /* set up yytext again */ \ } \ while ( 0 ) #define unput(c) yyunput( c, yyg->yytext_ptr , yyscanner ) +#ifndef YY_TYPEDEF_YY_SIZE_T +#define YY_TYPEDEF_YY_SIZE_T +typedef size_t yy_size_t; +#endif + #ifndef YY_STRUCT_YY_BUFFER_STATE #define YY_STRUCT_YY_BUFFER_STATE struct yy_buffer_state { /* %if-c-only */ FILE *yy_input_file; /* %endif */ /* %if-c++-only */ /* %endif */ char *yy_ch_buf; /* input buffer */ char *yy_buf_pos; /* current position in input buffer */ /* Size of input buffer in bytes, not including room for EOB * characters. */ yy_size_t yy_buf_size; /* Number of characters read into yy_ch_buf, not including EOB * characters. */ - yy_size_t yy_n_chars; + int yy_n_chars; /* Whether we "own" the buffer - i.e., we know we created it, * and can realloc() it to grow it, and should free() it to * delete it. */ int yy_is_our_buffer; /* Whether this is an "interactive" input source; if so, and * if we're using stdio for input, then we want to use getc() * instead of fread(), to make sure we stop fetching input after * each newline. */ int yy_is_interactive; /* Whether we're considered to be at the beginning of a line. * If so, '^' rules will be active on the next match, otherwise * not. */ int yy_at_bol; int yy_bs_lineno; /**< The line count. */ int yy_bs_column; /**< The column count. */ /* Whether to try to fill the input buffer when we reach the * end of it. */ int yy_fill_buffer; int yy_buffer_status; #define YY_BUFFER_NEW 0 #define YY_BUFFER_NORMAL 1 /* When an EOF's been seen but there's still some text to process * then we mark the buffer as YY_EOF_PENDING, to indicate that we * shouldn't try reading from the input source any more. We might * still have a bunch of tokens to match, though, because of * possible backing-up. * * When we actually see the EOF, we change the status to "new" * (via xhpastrestart()), so that the user can continue scanning by * just pointing yyin at a new input file. */ #define YY_BUFFER_EOF_PENDING 2 }; #endif /* !YY_STRUCT_YY_BUFFER_STATE */ /* %if-c-only Standard (non-C++) definition */ /* %not-for-header */ /* %if-not-reentrant */ /* %endif */ /* %ok-for-header */ /* %endif */ /* We provide macros for accessing buffer states in case in the * future we want to put the buffer states in a more general * "scanner state". * * Returns the top of the stack, or NULL. */ #define YY_CURRENT_BUFFER ( yyg->yy_buffer_stack \ ? yyg->yy_buffer_stack[yyg->yy_buffer_stack_top] \ : NULL) /* Same as previous macro, but useful when we know that the buffer stack is not * NULL or when we need an lvalue. For internal use only. */ #define YY_CURRENT_BUFFER_LVALUE yyg->yy_buffer_stack[yyg->yy_buffer_stack_top] /* %if-c-only Standard (non-C++) definition */ /* %if-not-reentrant */ /* %not-for-header */ /* %ok-for-header */ /* %endif */ void xhpastrestart (FILE *input_file ,yyscan_t yyscanner ); void xhpast_switch_to_buffer (YY_BUFFER_STATE new_buffer ,yyscan_t yyscanner ); YY_BUFFER_STATE xhpast_create_buffer (FILE *file,int size ,yyscan_t yyscanner ); void xhpast_delete_buffer (YY_BUFFER_STATE b ,yyscan_t yyscanner ); void xhpast_flush_buffer (YY_BUFFER_STATE b ,yyscan_t yyscanner ); void xhpastpush_buffer_state (YY_BUFFER_STATE new_buffer ,yyscan_t yyscanner ); void xhpastpop_buffer_state (yyscan_t yyscanner ); static void xhpastensure_buffer_stack (yyscan_t yyscanner ); static void xhpast_load_buffer_state (yyscan_t yyscanner ); static void xhpast_init_buffer (YY_BUFFER_STATE b,FILE *file ,yyscan_t yyscanner ); #define YY_FLUSH_BUFFER xhpast_flush_buffer(YY_CURRENT_BUFFER ,yyscanner) YY_BUFFER_STATE xhpast_scan_buffer (char *base,yy_size_t size ,yyscan_t yyscanner ); YY_BUFFER_STATE xhpast_scan_string (yyconst char *yy_str ,yyscan_t yyscanner ); -YY_BUFFER_STATE xhpast_scan_bytes (yyconst char *bytes,yy_size_t len ,yyscan_t yyscanner ); +YY_BUFFER_STATE xhpast_scan_bytes (yyconst char *bytes,int len ,yyscan_t yyscanner ); /* %endif */ void *xhpastalloc (yy_size_t ,yyscan_t yyscanner ); void *xhpastrealloc (void *,yy_size_t ,yyscan_t yyscanner ); void xhpastfree (void * ,yyscan_t yyscanner ); #define yy_new_buffer xhpast_create_buffer #define yy_set_interactive(is_interactive) \ { \ if ( ! YY_CURRENT_BUFFER ){ \ xhpastensure_buffer_stack (yyscanner); \ YY_CURRENT_BUFFER_LVALUE = \ xhpast_create_buffer(yyin,YY_BUF_SIZE ,yyscanner); \ } \ YY_CURRENT_BUFFER_LVALUE->yy_is_interactive = is_interactive; \ } #define yy_set_bol(at_bol) \ { \ if ( ! YY_CURRENT_BUFFER ){\ xhpastensure_buffer_stack (yyscanner); \ YY_CURRENT_BUFFER_LVALUE = \ xhpast_create_buffer(yyin,YY_BUF_SIZE ,yyscanner); \ } \ YY_CURRENT_BUFFER_LVALUE->yy_at_bol = at_bol; \ } #define YY_AT_BOL() (YY_CURRENT_BUFFER_LVALUE->yy_at_bol) /* %% [1.0] yytext/yyin/yyout/yy_state_type/yylineno etc. def's & init go here */ /* Begin user sect3 */ #define xhpastwrap(n) 1 #define YY_SKIP_YYWRAP #define FLEX_DEBUG typedef unsigned char YY_CHAR; typedef int yy_state_type; #define yytext_ptr yytext_r /* %if-c-only Standard (non-C++) definition */ static yy_state_type yy_get_previous_state (yyscan_t yyscanner ); static yy_state_type yy_try_NUL_trans (yy_state_type current_state ,yyscan_t yyscanner); static int yy_get_next_buffer (yyscan_t yyscanner ); static void yy_fatal_error (yyconst char msg[] ,yyscan_t yyscanner ); /* %endif */ /* Done after the current pattern has been matched and before the * corresponding action - sets up yytext. */ #define YY_DO_BEFORE_ACTION \ yyg->yytext_ptr = yy_bp; \ /* %% [2.0] code to fiddle yytext and yyleng for yymore() goes here \ */\ yyg->yytext_ptr -= yyg->yy_more_len; \ yyleng = (size_t) (yy_cp - yyg->yytext_ptr); \ yyg->yy_hold_char = *yy_cp; \ *yy_cp = '\0'; \ /* %% [3.0] code to copy yytext_ptr to yytext[] goes here, if %array \ */\ yyg->yy_c_buf_p = yy_cp; /* %% [4.0] data tables for the DFA and the user's section 1 definitions go here */ #define YY_NUM_RULES 140 #define YY_END_OF_BUFFER 141 /* This struct is not used in this scanner, but its presence is necessary. */ struct yy_trans_info { flex_int32_t yy_verify; flex_int32_t yy_nxt; }; static yyconst flex_int16_t yy_accept[596] = { 0, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 141, 6, 6, 139, 12, 139, 139, 9, 139, 139, 139, 139, 139, 139, 139, 139, 139, 139, 125, 125, 139, 139, 139, 139, 139, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 115, 139, 127, 139, 139, 18, 17, 17, 18, 14, 13, 13, 16, 18, 139, 139, 133, 134, 134, 137, 138, 138, 137, 139, 135, 135, 127, 6, 4, 2, 12, 103, 0, 129, 0, 128, 94, 8, 101, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 91, 110, 89, 111, 90, 112, 126, 93, 11, 9, 92, 126, 125, 0, 0, 114, 0, 108, 106, 103, 102, 113, 107, 109, 7, 127, 127, 127, 127, 48, 0, 0, 0, 127, 127, 127, 127, 127, 39, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 34, 127, 127, 127, 127, 127, 127, 26, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 97, 127, 0, 130, 96, 100, 18, 17, 20, 14, 13, 15, 19, 0, 0, 133, 134, 137, 138, 136, 136, 137, 137, 0, 135, 135, 5, 3, 0, 105, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 126, 0, 126, 125, 0, 131, 98, 104, 99, 7, 7, 127, 28, 127, 0, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 42, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 31, 127, 127, 127, 127, 127, 127, 127, 127, 59, 127, 62, 70, 127, 27, 127, 127, 127, 127, 127, 127, 127, 132, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 131, 127, 127, 127, 51, 127, 127, 127, 127, 127, 127, 127, 38, 36, 127, 127, 127, 127, 127, 127, 23, 33, 127, 127, 127, 127, 127, 55, 127, 127, 127, 127, 127, 79, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 0, 0, 0, 0, 0, 0, 0, 116, 0, 0, 0, 0, 0, 0, 0, 127, 80, 53, 60, 75, 32, 57, 127, 127, 127, 127, 73, 127, 127, 37, 127, 127, 127, 66, 127, 127, 127, 127, 127, 127, 127, 72, 127, 29, 127, 127, 127, 127, 127, 127, 127, 61, 71, 40, 127, 127, 127, 127, 127, 127, 127, 127, 1, 1, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 117, 0, 0, 0, 0, 127, 127, 127, 127, 35, 127, 43, 127, 127, 127, 127, 127, 63, 127, 127, 127, 127, 127, 127, 127, 69, 127, 58, 64, 49, 127, 127, 127, 127, 127, 127, 127, 127, 0, 121, 0, 0, 0, 0, 0, 0, 0, 0, 124, 0, 127, 127, 46, 52, 127, 127, 127, 127, 77, 44, 127, 127, 21, 127, 127, 127, 67, 127, 24, 127, 88, 127, 127, 127, 127, 127, 127, 0, 120, 0, 0, 0, 122, 0, 118, 0, 0, 65, 54, 127, 127, 127, 41, 56, 127, 127, 127, 127, 127, 127, 127, 127, 85, 127, 127, 84, 127, 127, 0, 119, 0, 127, 127, 50, 127, 127, 127, 76, 86, 68, 127, 81, 127, 127, 127, 127, 47, 45, 78, 127, 30, 127, 127, 127, 82, 127, 127, 127, 127, 127, 127, 22, 25, 83, 127, 127, 127, 87, 127, 74, 0 } ; static yyconst flex_int16_t yy_base[640] = { 0, 955, 953, 0, 0, 246, 247, 248, 252, 253, 254, 315, 441, 258, 259, 567, 693, 260, 264, 819, 945, 0, 0, 0, 0, 1010, 0, 238,12788, 270, 875, 980,12788, 0, 220, 238, 341, 1008, 874, 242, 245, 333, 244, 976, 460, 876, 352, 231, 236, 871, 441, 984, 326, 324, 565, 319, 321, 0, 448, 205, 568, 313, 445, 327, 551, 562, 562, 455, 449, 443,12788, 870, 835, 833, 951, 0,12788, 917, 879, 0,12788, 911, 858, 872, 0, 0, 0,12788, 908, 0,12788, 906, 1125, 274,12788, 904, 618, 0, 749, 575, 749, 747, 987,12788, 0, 0,12788,12788,12788,12788, 721, 0, 1173, 446, 575, 560, 571, 571, 584, 587, 677, 685,12788,12788,12788,12788,12788,12788, 838,12788, 765, 12788,12788, 998, 1144, 721, 1243,12788, 682, 248,12788, 12788, 745,12788,12788, 744, 405, 749, 1142, 1024, 1172, 862, 1008, 845, 744, 1173, 1242, 1267, 1169, 1170, 863, 1243, 1289, 1225, 1295, 1023, 1296, 1229, 1299, 1314, 1326, 1327, 864, 1328, 1336, 1334, 1350, 1337, 1368, 865, 1384, 1382, 1393, 1402, 1400, 1411, 1422, 1425, 1444, 1436, 1442, 1452,12788, 1492, 707,12788,12788,12788, 0,12788,12788, 0,12788,12788,12788, 768, 762, 0,12788, 0,12788, 12788, 788, 1602, 406, 407,12788, 786,12788,12788, 695, 12788, 1022, 0, 1020, 686, 701, 701, 697, 821, 822, 834, 844, 983, 1310, 1488, 1602, 492, 1523, 1664, 1018, 783,12788,12788,12788,12788, 784, 1002, 0, 1024, 731, 1113, 1122, 1161, 1151, 1157, 1235, 1228, 1255, 1243, 1308, 1306, 1446, 1322, 1315, 1337, 1361, 1378, 1401, 1424, 1413, 1427, 1431, 1426, 1448, 1452, 1439, 1485, 0, 1589, 1472, 1481, 1473, 1478, 1483, 1584, 1590, 0, 1601, 0, 0, 1596, 0, 1597, 1601, 1605, 1611, 1604, 1610, 1626,12788, 756, 650, 1612, 1670, 1671, 1661, 1672, 1674, 1740, 1672, 1666, 1670, 1677, 1676, 1746, 1665, 896, 1666, 1661, 1676, 0, 1680, 1670, 1685, 1673, 1685, 1695, 1702, 0, 1715, 1702, 1724, 1715, 1725, 1709, 1725, 0, 0, 1720, 1723, 1735, 1717, 1737, 0, 1734, 1719, 1740, 1724, 1723, 0, 1725, 1726, 1746, 1743, 1740, 1741, 1733, 1743, 1750, 1731, 1737, 1753, 1759, 1769, 1776, 1777, 1781, 1780, 1775, 1783, 1853, 1772, 1782, 1856, 1791, 1784, 1862,12788, 1798, 1803, 1863, 1795, 1795, 1791, 1803, 1812, 0, 0, 0, 0, 0, 0, 1800, 1797, 1804, 1811, 0, 1815, 1804, 0, 1825, 1842, 1848, 0, 1850, 1846, 1844, 1844, 1854, 1845, 1854, 0, 1846, 0, 1843, 1861, 1862, 1849, 1854, 1866, 1862, 0, 0, 0, 1852, 589, 1867, 1870, 1854, 1870, 1868, 1872,12788, 658, 1953, 1853, 1966,12788, 1879, 1876, 1969, 1881, 1895, 1971,12788, 1910, 1915, 1977, 1904, 1920, 1903, 1920, 1906, 0, 1915, 1923, 1909, 1919, 1913, 1925, 1920, 0, 1931, 1933, 1937, 1940, 1941, 1939, 1930, 0, 1948, 0, 0, 0, 1943, 567, 551, 1946, 443, 441, 1954, 1952, 2032,12788, 2034, 1961, 2036, 1960, 2038, 2044, 1979, 2046,12788, 1966, 1967, 1983, 0, 0, 1989, 1991, 1990, 1990, 0, 0, 1982, 1983, 336, 1994, 2002, 2005, 0, 2012, 331, 327, 0, 326, 2017, 2027, 315, 2028, 2017, 2089,12788, 2091, 2093, 2099,12788, 2101,12788, 2103, 2105, 0, 0, 2024, 2040, 2037, 0, 0, 2027, 2037, 2038, 2050, 2051, 2056, 2048, 306, 0, 2057, 2066, 0, 219, 2081, 2138,12788, 2144, 2081, 2083, 0, 2075, 2082, 2091, 0, 0, 0, 2084, 0, 2085, 2087, 218, 2098, 0, 0, 0, 2099, 0, 2102, 217, 2091, 0, 2103, 2104, 2106, 209, 2107, 205, 0, 0, 0, 2105, 199, 2114, 0, 2102, 0,12788, 2216, 2471, 2726, 2981, 3236, 3491, 3746, 4001, 4256, 4511, 4702, 4957, 5165, 5420, 5675, 5930, 6121, 6312, 6520, 6775, 7030, 7285, 7507, 7762, 8017, 8272, 8480, 8735, 8990, 9245, 9500, 9755, 9977,10194,10402, 10657,10912,11167,11422,11630,11885,12093,12315,12532 } ; static yyconst flex_int16_t yy_def[640] = { 0, 596, 596, 595, 3, 597, 597, 598, 598, 597, 597, 599, 599, 600, 600, 601, 601, 602, 602, 603, 603, 20, 20, 600, 600, 595, 604, 595, 595, 595, 595, 605, 595, 606, 595, 595, 607, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 595, 595, 608, 609, 595, 610, 595, 595, 595, 611, 595, 595, 595, 595, 612, 613, 614, 595, 595, 615, 595, 595, 616, 617, 595, 595, 618, 619, 595, 595, 595, 595, 620, 595, 621, 622, 595, 595, 595, 595, 623, 624, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 618, 618, 618, 618, 618, 620, 623, 595, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 595, 618, 625, 595, 595, 595, 626, 595, 595, 627, 595, 595, 595, 628, 629, 630, 595, 631, 595, 595, 595, 632, 631, 633, 595, 595, 595, 595, 595, 595, 634, 635, 636, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 638, 639, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 595, 595, 595, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 0, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595 } ; static yyconst flex_int16_t yy_nxt[13045] = { 0, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 28, 28, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 28, 38, 39, 28, 40, 41, 42, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 28, 46, 47, 48, 49, 28, 50, 51, 52, 53, 54, 55, 56, 57, 58, 57, 57, 59, 57, 60, 61, 62, 57, 63, 64, 65, 66, 67, 68, 69, 57, 57, 28, 70, 28, 71, 72, 73, 50, 51, 52, 53, 54, 55, 56, 57, 58, 57, 57, 59, 57, 60, 61, 62, 57, 63, 64, 65, 66, 67, 68, 69, 57, 57, 28, 74, 28, 28, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 76, 76, 80, 77, 77, 81, 80, 76, 76, 81, 77, 77, 87, 87, 94, 88, 88, 95, 94, 98, 108, 95, 176, 100, 100, 106, 107, 100, 216, 123, 130, 217, 78, 78, 125, 131, 142, 143, 592, 83, 83, 144, 145, 109, 590, 99, 100, 124, 588, 132, 126, 127, 241, 242, 176, 82, 583, 579, 569, 82, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 84, 28, 28, 28, 28, 85, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 103, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 156, 167, 159, 129, 179, 182, 170, 168, 138, 171, 566, 157, 160, 169, 158, 28, 28, 28, 28, 550, 28, 139, 140, 141, 245, 211, 216, 246, 212, 217, 547, 546, 156, 167, 159, 545, 179, 182, 170, 168, 540, 171, 111, 157, 160, 169, 158, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 84, 28, 28, 28, 28, 85, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 133, 148, 134, 134, 134, 134, 134, 134, 134, 134, 134, 134, 172, 149, 189, 190, 191, 150, 151, 173, 174, 180, 225, 135, 181, 175, 28, 28, 28, 28, 519, 28, 518, 148, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 172, 149, 189, 190, 191, 150, 151, 173, 174, 180, 225, 135, 181, 175, 28, 28, 28, 28, 89, 89, 89, 89, 89, 89, 89, 89, 89, 90, 89, 89, 91, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 161, 177, 185, 183, 219, 178, 184, 228, 187, 162, 163, 164, 186, 188, 516, 229, 226, 230, 231, 165, 152, 166, 227, 220, 232, 153, 89, 89, 89, 89, 515, 89, 161, 177, 185, 183, 433, 178, 184, 228, 187, 162, 163, 164, 186, 188, 154, 229, 226, 230, 231, 165, 476, 166, 227, 220, 232, 300, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 90, 89, 89, 91, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 89, 100, 100, 103, 233, 100, 234, 237, 240, 237, 303, 304, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 305, 306, 100, 307, 595, 89, 89, 89, 89, 595, 89, 300, 241, 317, 233, 245, 234, 216, 240, 211, 303, 304, 300, 300, 195, 250, 244, 243, 235, 221, 595, 218, 305, 306, 111, 307, 317, 89, 89, 89, 89, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 28, 28, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 28, 38, 39, 28, 40, 41, 42, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 28, 46, 47, 48, 49, 28, 103, 96, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 595, 595, 595, 595, 308, 595, 595, 595, 595, 317, 309, 135, 310, 311, 28, 70, 28, 71, 216, 73, 210, 96, 208, 204, 203, 202, 595, 595, 595, 595, 200, 199, 317, 195, 193, 192, 308, 146, 137, 122, 101, 111, 309, 135, 310, 311, 28, 74, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 28, 28, 29, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 28, 38, 39, 28, 40, 41, 42, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 28, 46, 47, 48, 49, 28, 595, 96, 196, 27, 103, 27, 595, 112, 152, 595, 595, 103, 133, 153, 134, 134, 134, 134, 134, 134, 134, 134, 134, 134, 595, 595, 28, 70, 28, 71, 112, 73, 103, 96, 154, 135, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 103, 595, 595, 103, 595, 595, 595, 595, 136, 312, 155, 135, 28, 74, 28, 28, 104, 113, 114, 197, 115, 135, 116, 104, 595, 117, 595, 595, 595, 316, 318, 118, 263, 319, 119, 120, 248, 121, 595, 595, 136, 312, 155, 135, 104, 595, 595, 595, 595, 113, 114, 595, 115, 595, 116, 595, 111, 117, 104, 595, 595, 316, 318, 118, 263, 319, 119, 120, 248, 121, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 209, 212, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 595, 595, 595, 595, 595, 320, 595, 595, 595, 112, 209, 214, 209, 209, 209, 209, 209, 133, 321, 134, 134, 134, 134, 134, 134, 134, 134, 134, 134, 595, 595, 595, 112, 595, 595, 595, 595, 320, 595, 595, 135, 595, 595, 209, 209, 209, 209, 595, 209, 595, 321, 595, 247, 595, 595, 322, 595, 595, 595, 595, 595, 323, 324, 595, 257, 113, 114, 258, 115, 251, 116, 595, 135, 117, 256, 209, 209, 209, 209, 118, 595, 249, 119, 120, 247, 121, 595, 322, 595, 595, 595, 595, 595, 323, 324, 595, 257, 113, 114, 258, 115, 251, 116, 595, 595, 117, 256, 595, 595, 595, 595, 118, 595, 249, 119, 120, 595, 121, 239, 239, 239, 239, 239, 239, 239, 239, 239, 239, 595, 595, 595, 327, 261, 595, 266, 239, 239, 239, 239, 239, 239, 595, 259, 595, 595, 325, 326, 328, 595, 329, 595, 595, 252, 253, 595, 595, 595, 595, 595, 254, 595, 595, 595, 327, 261, 595, 266, 239, 239, 239, 239, 239, 239, 255, 259, 595, 595, 325, 326, 328, 595, 329, 595, 595, 252, 253, 595, 595, 595, 595, 262, 254, 595, 595, 595, 595, 264, 595, 595, 260, 595, 595, 595, 595, 330, 255, 595, 265, 267, 595, 313, 595, 595, 595, 595, 595, 595, 331, 595, 268, 314, 595, 262, 595, 595, 337, 338, 595, 264, 595, 272, 260, 269, 339, 595, 271, 330, 595, 270, 265, 267, 277, 313, 595, 275, 595, 273, 274, 595, 331, 595, 268, 314, 340, 595, 595, 595, 337, 338, 595, 276, 595, 272, 595, 269, 339, 595, 271, 595, 595, 270, 595, 595, 277, 341, 281, 275, 595, 273, 274, 595, 595, 278, 595, 279, 340, 595, 595, 595, 595, 280, 595, 276, 595, 284, 342, 595, 595, 595, 595, 285, 282, 595, 595, 283, 595, 341, 281, 595, 595, 595, 595, 595, 595, 278, 595, 279, 343, 595, 344, 286, 595, 280, 595, 315, 315, 284, 342, 315, 595, 345, 595, 285, 282, 346, 288, 283, 347, 287, 595, 289, 332, 291, 333, 348, 290, 334, 315, 349, 343, 350, 344, 286, 595, 595, 595, 335, 595, 595, 595, 336, 292, 345, 595, 595, 595, 346, 288, 595, 347, 287, 595, 289, 332, 291, 333, 348, 290, 334, 595, 349, 351, 350, 354, 355, 356, 293, 294, 335, 295, 357, 296, 336, 292, 358, 297, 298, 299, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 595, 595, 595, 595, 595, 351, 595, 354, 355, 356, 293, 294, 595, 295, 357, 296, 595, 595, 358, 297, 298, 299, 209, 209, 209, 209, 209, 209, 209, 209, 209, 211, 209, 209, 212, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 209, 214, 209, 209, 209, 209, 209, 352, 359, 360, 361, 135, 362, 363, 364, 353, 367, 368, 365, 369, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 366, 370, 371, 209, 209, 209, 209, 595, 209, 352, 359, 360, 361, 135, 362, 363, 364, 353, 367, 368, 365, 369, 239, 239, 239, 239, 239, 239, 239, 239, 239, 239, 366, 370, 371, 209, 209, 209, 209, 239, 239, 239, 239, 239, 239, 372, 373, 374, 375, 376, 595, 380, 381, 382, 383, 384, 595, 385, 386, 377, 387, 388, 389, 390, 391, 315, 315, 392, 393, 315, 394, 239, 239, 239, 239, 239, 239, 372, 373, 374, 375, 376, 377, 380, 381, 382, 383, 384, 315, 385, 386, 378, 387, 388, 389, 390, 391, 395, 396, 392, 393, 397, 394, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 379, 414, 415, 416, 417, 418, 419, 420, 421, 422, 395, 396, 423, 424, 397, 425, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 379, 414, 415, 416, 417, 418, 419, 420, 421, 422, 426, 427, 423, 424, 428, 425, 429, 430, 431, 432, 435, 433, 433, 436, 437, 434, 440, 441, 442, 443, 377, 444, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 426, 427, 433, 456, 428, 437, 429, 430, 431, 432, 435, 377, 444, 436, 438, 457, 440, 441, 442, 443, 378, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 458, 459, 460, 456, 461, 462, 463, 464, 465, 466, 439, 467, 468, 469, 470, 457, 471, 472, 473, 474, 475, 477, 478, 479, 480, 481, 482, 485, 595, 486, 487, 595, 458, 459, 460, 488, 461, 462, 463, 464, 465, 466, 439, 467, 468, 469, 470, 483, 471, 472, 473, 474, 475, 477, 478, 479, 480, 481, 482, 485, 437, 486, 487, 444, 489, 444, 490, 488, 491, 494, 483, 492, 495, 496, 497, 498, 499, 500, 501, 484, 502, 503, 504, 437, 505, 506, 444, 507, 444, 508, 509, 510, 438, 511, 492, 445, 489, 445, 490, 512, 491, 494, 513, 493, 495, 496, 497, 498, 499, 500, 501, 514, 502, 503, 504, 517, 505, 506, 520, 507, 521, 508, 509, 510, 524, 511, 483, 525, 522, 595, 444, 512, 526, 530, 513, 531, 532, 533, 528, 534, 492, 535, 536, 514, 537, 538, 539, 517, 541, 483, 520, 522, 521, 444, 542, 526, 524, 543, 484, 525, 523, 528, 445, 492, 527, 530, 544, 531, 532, 533, 529, 534, 493, 535, 536, 548, 537, 538, 539, 549, 541, 551, 552, 522, 595, 437, 542, 377, 595, 543, 595, 556, 557, 526, 558, 528, 559, 553, 544, 555, 555, 560, 561, 555, 562, 563, 522, 548, 437, 564, 377, 549, 565, 551, 552, 523, 526, 438, 528, 378, 553, 567, 555, 556, 557, 527, 558, 529, 559, 554, 568, 570, 553, 560, 561, 571, 562, 563, 555, 555, 572, 564, 555, 573, 565, 574, 575, 576, 577, 578, 580, 581, 146, 567, 582, 553, 584, 585, 586, 595, 587, 555, 568, 570, 554, 589, 591, 571, 593, 594, 595, 595, 572, 595, 595, 573, 595, 574, 575, 576, 577, 578, 580, 581, 595, 595, 582, 595, 584, 585, 586, 146, 587, 595, 595, 595, 595, 589, 591, 595, 593, 594, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 75, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 79, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 86, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 595, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 595, 595, 595, 595, 105, 595, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 595, 595, 595, 595, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 198, 198, 198, 198, 198, 198, 198, 198, 198, 595, 198, 198, 595, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 595, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 201, 201, 201, 201, 201, 201, 201, 201, 201, 595, 201, 201, 595, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 595, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 595, 595, 595, 595, 205, 595, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 595, 595, 595, 595, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 205, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 595, 595, 595, 595, 206, 595, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 595, 595, 595, 595, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 206, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 595, 595, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 207, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 209, 209, 209, 209, 209, 209, 209, 209, 209, 595, 209, 209, 595, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 147, 595, 595, 595, 595, 147, 595, 595, 595, 595, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 147, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 595, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 97, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 222, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 595, 595, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 223, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 224, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 194, 198, 198, 198, 198, 198, 198, 198, 198, 198, 595, 198, 198, 595, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 595, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 198, 201, 201, 201, 201, 201, 201, 201, 201, 201, 595, 201, 201, 595, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 595, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 201, 301, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 301, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 302, 595, 595, 595, 595, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 302, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 595, 595, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 207, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 595, 595, 595, 595, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 207, 209, 209, 209, 209, 209, 209, 209, 209, 209, 595, 209, 209, 595, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 209, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 215, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 595, 595, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 223, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 595, 595, 595, 595, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 223, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 110, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 595, 595, 595, 595, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 147, 301, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 301, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 595, 595, 595, 595, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 301, 302, 595, 595, 595, 595, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 302, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 595, 595, 595, 595, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 302, 25, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595 } ; static yyconst flex_int16_t yy_chk[13045] = { 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 5, 6, 7, 5, 6, 7, 8, 9, 10, 8, 9, 10, 13, 14, 17, 13, 14, 17, 18, 27, 35, 18, 59, 29, 29, 34, 34, 29, 93, 39, 42, 93, 5, 6, 40, 42, 47, 47, 590, 9, 10, 48, 48, 35, 585, 27, 29, 39, 583, 42, 40, 40, 139, 139, 59, 7, 577, 569, 551, 8, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 36, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 52, 55, 53, 41, 61, 63, 56, 55, 46, 56, 546, 52, 53, 55, 52, 11, 11, 11, 11, 519, 11, 46, 46, 46, 146, 214, 215, 146, 214, 215, 516, 514, 52, 55, 53, 513, 61, 63, 56, 55, 507, 56, 36, 52, 53, 55, 52, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 44, 50, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 58, 50, 67, 68, 69, 50, 50, 58, 58, 62, 113, 44, 62, 58, 12, 12, 12, 12, 480, 12, 479, 50, 237, 237, 237, 237, 237, 237, 237, 237, 237, 237, 58, 50, 67, 68, 69, 50, 50, 58, 58, 62, 113, 44, 62, 58, 12, 12, 12, 12, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 54, 60, 65, 64, 99, 60, 64, 115, 66, 54, 54, 54, 65, 66, 477, 116, 114, 117, 118, 54, 96, 54, 114, 99, 119, 96, 15, 15, 15, 15, 476, 15, 54, 60, 65, 64, 434, 60, 64, 115, 66, 54, 54, 54, 65, 66, 96, 116, 114, 117, 118, 54, 426, 54, 114, 99, 119, 302, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 100, 100, 110, 120, 100, 121, 135, 138, 135, 220, 225, 135, 135, 135, 135, 135, 135, 135, 135, 135, 135, 226, 227, 100, 228, 147, 16, 16, 16, 16, 147, 16, 301, 250, 241, 120, 246, 121, 217, 138, 212, 220, 225, 206, 205, 194, 154, 145, 142, 130, 101, 147, 98, 226, 227, 110, 228, 241, 16, 16, 16, 16, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 153, 19, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 151, 160, 172, 179, 229, 151, 160, 172, 179, 317, 230, 128, 231, 232, 19, 19, 19, 19, 95, 19, 91, 19, 88, 83, 82, 81, 151, 160, 172, 179, 78, 77, 317, 73, 72, 71, 229, 49, 45, 38, 30, 153, 230, 128, 231, 232, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 25, 20, 74, 2, 31, 1, 0, 37, 51, 0, 0, 102, 43, 51, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 0, 0, 20, 20, 20, 20, 37, 20, 152, 20, 51, 43, 133, 133, 133, 133, 133, 133, 133, 133, 133, 133, 222, 165, 149, 224, 0, 0, 165, 149, 43, 233, 51, 133, 20, 20, 20, 20, 31, 37, 37, 74, 37, 43, 37, 102, 0, 37, 0, 165, 149, 240, 247, 37, 165, 249, 37, 37, 149, 37, 0, 0, 43, 233, 51, 133, 152, 0, 0, 0, 0, 37, 37, 0, 37, 0, 37, 0, 224, 37, 222, 0, 0, 240, 247, 37, 165, 249, 37, 37, 149, 37, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 92, 0, 0, 0, 148, 0, 251, 0, 0, 148, 112, 92, 92, 92, 92, 92, 92, 92, 134, 252, 134, 134, 134, 134, 134, 134, 134, 134, 134, 134, 148, 158, 159, 112, 150, 155, 158, 159, 251, 150, 155, 134, 0, 0, 92, 92, 92, 92, 0, 92, 0, 252, 0, 148, 0, 0, 253, 158, 159, 0, 150, 155, 254, 255, 0, 159, 112, 112, 159, 112, 155, 112, 0, 134, 112, 158, 92, 92, 92, 92, 112, 0, 150, 112, 112, 148, 112, 163, 253, 0, 0, 167, 163, 0, 254, 255, 167, 159, 112, 112, 159, 112, 155, 112, 156, 161, 112, 158, 0, 156, 161, 0, 112, 163, 150, 112, 112, 167, 112, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 157, 156, 161, 257, 163, 157, 167, 136, 136, 136, 136, 136, 136, 0, 161, 0, 0, 256, 256, 258, 0, 259, 162, 0, 156, 156, 157, 162, 164, 166, 0, 157, 168, 164, 166, 257, 163, 168, 167, 136, 136, 136, 136, 136, 136, 157, 161, 169, 162, 256, 256, 258, 169, 259, 164, 166, 156, 156, 168, 170, 171, 173, 164, 157, 170, 171, 173, 175, 166, 174, 177, 162, 175, 169, 174, 177, 260, 157, 0, 166, 168, 0, 234, 176, 0, 170, 171, 173, 176, 261, 0, 169, 234, 175, 164, 174, 177, 263, 264, 0, 166, 178, 174, 162, 170, 265, 178, 173, 260, 176, 171, 166, 168, 177, 234, 181, 175, 180, 174, 174, 181, 261, 180, 169, 234, 266, 182, 178, 0, 263, 264, 182, 176, 184, 174, 183, 170, 265, 184, 173, 183, 181, 171, 180, 185, 177, 267, 181, 175, 185, 174, 174, 182, 0, 178, 186, 180, 266, 187, 184, 186, 183, 180, 187, 176, 0, 183, 268, 0, 189, 185, 0, 184, 182, 189, 190, 182, 188, 267, 181, 190, 186, 188, 0, 187, 191, 178, 0, 180, 269, 191, 270, 185, 0, 180, 189, 235, 235, 183, 268, 235, 190, 271, 188, 184, 182, 272, 187, 182, 273, 186, 191, 188, 262, 190, 262, 274, 189, 262, 235, 275, 269, 276, 270, 185, 193, 0, 0, 262, 0, 193, 0, 262, 191, 271, 0, 0, 0, 272, 187, 0, 273, 186, 0, 188, 262, 190, 262, 274, 189, 262, 193, 275, 277, 276, 280, 281, 282, 193, 193, 262, 193, 283, 193, 262, 191, 284, 193, 193, 193, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 0, 0, 0, 0, 0, 277, 0, 280, 281, 282, 193, 193, 0, 193, 283, 193, 0, 0, 284, 193, 193, 193, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 213, 236, 236, 236, 236, 236, 236, 236, 236, 236, 236, 213, 213, 213, 213, 213, 213, 213, 279, 285, 286, 288, 236, 291, 293, 294, 279, 296, 297, 295, 298, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 295, 299, 303, 213, 213, 213, 213, 0, 213, 279, 285, 286, 288, 236, 291, 293, 294, 279, 296, 297, 295, 298, 239, 239, 239, 239, 239, 239, 239, 239, 239, 239, 295, 299, 303, 213, 213, 213, 213, 239, 239, 239, 239, 239, 239, 304, 305, 306, 307, 308, 0, 310, 311, 312, 313, 314, 0, 316, 318, 309, 319, 320, 322, 323, 324, 315, 315, 325, 326, 315, 327, 239, 239, 239, 239, 239, 239, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 318, 309, 319, 320, 322, 323, 324, 328, 330, 325, 326, 331, 327, 332, 333, 334, 335, 336, 339, 340, 341, 342, 343, 345, 346, 347, 348, 349, 351, 309, 352, 353, 354, 355, 356, 357, 358, 359, 360, 328, 330, 361, 362, 331, 363, 332, 333, 334, 335, 336, 339, 340, 341, 342, 343, 345, 346, 347, 348, 349, 351, 309, 352, 353, 354, 355, 356, 357, 358, 359, 360, 364, 365, 361, 362, 366, 363, 367, 368, 369, 370, 372, 371, 371, 373, 374, 371, 375, 376, 379, 380, 377, 381, 382, 383, 384, 385, 386, 393, 394, 395, 396, 398, 364, 365, 371, 399, 366, 374, 367, 368, 369, 370, 372, 377, 381, 373, 374, 401, 375, 376, 379, 380, 377, 381, 382, 383, 384, 385, 386, 393, 394, 395, 396, 398, 402, 403, 405, 399, 406, 407, 408, 409, 410, 411, 374, 413, 415, 416, 417, 401, 418, 419, 420, 421, 425, 427, 428, 429, 430, 431, 432, 436, 0, 439, 440, 0, 402, 403, 405, 442, 406, 407, 408, 409, 410, 411, 374, 413, 415, 416, 417, 435, 418, 419, 420, 421, 425, 427, 428, 429, 430, 431, 432, 436, 437, 439, 440, 441, 443, 444, 446, 442, 447, 449, 435, 448, 450, 451, 452, 453, 455, 456, 457, 435, 458, 459, 460, 437, 461, 463, 441, 464, 444, 465, 466, 467, 437, 468, 448, 441, 443, 444, 446, 469, 447, 449, 471, 448, 450, 451, 452, 453, 455, 456, 457, 475, 458, 459, 460, 478, 461, 463, 481, 464, 482, 465, 466, 467, 486, 468, 483, 488, 485, 0, 487, 469, 489, 491, 471, 494, 495, 496, 490, 499, 492, 500, 501, 475, 502, 505, 506, 478, 508, 483, 481, 485, 482, 487, 509, 489, 486, 510, 483, 488, 485, 490, 487, 492, 489, 491, 512, 494, 495, 496, 490, 499, 492, 500, 501, 517, 502, 505, 506, 518, 508, 520, 521, 522, 0, 524, 509, 525, 0, 510, 0, 534, 535, 526, 536, 528, 539, 530, 512, 531, 531, 540, 541, 531, 542, 543, 522, 517, 524, 544, 525, 518, 545, 520, 521, 522, 526, 524, 528, 525, 530, 548, 531, 534, 535, 526, 536, 528, 539, 530, 549, 552, 553, 540, 541, 556, 542, 543, 555, 555, 557, 544, 555, 559, 545, 560, 561, 565, 567, 568, 570, 574, 531, 548, 576, 553, 578, 580, 581, 0, 582, 555, 549, 552, 553, 584, 589, 556, 591, 593, 0, 0, 557, 0, 0, 559, 0, 560, 561, 565, 567, 568, 570, 574, 0, 0, 576, 0, 578, 580, 581, 555, 582, 0, 0, 0, 0, 584, 589, 0, 591, 593, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 596, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 597, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 598, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 599, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 601, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 602, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 603, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 0, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 604, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 605, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 0, 0, 0, 0, 606, 0, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 0, 0, 0, 0, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 606, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 607, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 0, 0, 0, 0, 0, 0, 0, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 0, 0, 0, 0, 608, 0, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 0, 0, 0, 0, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 608, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 609, 610, 610, 610, 610, 610, 610, 610, 610, 610, 0, 610, 610, 0, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 0, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 610, 611, 611, 611, 611, 611, 611, 611, 611, 611, 0, 611, 611, 0, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 0, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 611, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 0, 0, 0, 0, 612, 0, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 0, 0, 0, 0, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 612, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 0, 0, 0, 0, 613, 0, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 0, 0, 0, 0, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 613, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 0, 0, 0, 0, 0, 0, 0, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 0, 0, 0, 0, 614, 0, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 0, 0, 0, 0, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 614, 615, 615, 615, 615, 615, 615, 615, 615, 615, 0, 615, 615, 0, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 615, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 616, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 617, 618, 0, 0, 0, 0, 618, 0, 0, 0, 0, 0, 0, 0, 0, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 0, 0, 618, 0, 0, 0, 0, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 0, 0, 0, 0, 618, 0, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 0, 0, 0, 0, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 618, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 0, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 619, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 620, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 621, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 0, 0, 0, 0, 0, 0, 0, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 0, 0, 0, 0, 622, 0, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 0, 0, 0, 0, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 622, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 623, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 624, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 625, 626, 626, 626, 626, 626, 626, 626, 626, 626, 0, 626, 626, 0, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 0, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 626, 627, 627, 627, 627, 627, 627, 627, 627, 627, 0, 627, 627, 0, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 0, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 627, 628, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 0, 0, 0, 0, 0, 0, 0, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 0, 0, 0, 0, 628, 0, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 0, 0, 0, 0, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 628, 629, 0, 0, 0, 0, 0, 0, 0, 0, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 0, 0, 0, 0, 0, 0, 0, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 0, 0, 0, 0, 629, 0, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 0, 0, 0, 0, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 629, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 0, 0, 0, 0, 0, 0, 0, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 0, 0, 0, 0, 630, 0, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 0, 0, 0, 0, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 630, 631, 631, 631, 631, 631, 631, 631, 631, 631, 0, 631, 631, 0, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 631, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 632, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 633, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 634, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 0, 0, 0, 0, 0, 0, 0, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 0, 0, 0, 0, 635, 0, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 0, 0, 0, 0, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 635, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 636, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 0, 0, 0, 0, 0, 0, 0, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 0, 0, 0, 0, 637, 0, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 0, 0, 0, 0, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 637, 638, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 0, 0, 0, 0, 0, 0, 0, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 0, 0, 0, 0, 638, 0, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 0, 0, 0, 0, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 638, 639, 0, 0, 0, 0, 0, 0, 0, 0, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 0, 0, 0, 0, 0, 0, 0, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 0, 0, 0, 0, 639, 0, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 0, 0, 0, 0, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 639, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595, 595 } ; static yyconst yy_state_type yy_NUL_trans[595] = { 0, 26, 26, 28, 28, 75, 75, 79, 79, 75, 75, 28, 28, 28, 28, 89, 89, 93, 93, 28, 28, 28, 28, 28, 28, 0, 97, 0, 0, 0, 0, 102, 0, 0, 0, 0, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 194, 0, 198, 0, 0, 0, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 209, 0, 0, 209, 215, 0, 0, 0, 97, 0, 0, 0, 0, 102, 0, 222, 0, 0, 0, 0, 0, 110, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 102, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 194, 0, 0, 0, 198, 0, 0, 201, 0, 0, 0, 0, 0, 0, 0, 209, 0, 0, 0, 209, 209, 215, 0, 0, 0, 0, 0, 0, 102, 0, 110, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } ; static yyconst flex_int16_t yy_rule_linenum[140] = { 0, - 95, 100, 107, 114, 121, 128, 134, 138, 150, 154, - 159, 163, 173, 178, 179, 184, 187, 191, 193, 201, - 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, - 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, - 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, - 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, - 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, - 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, - 272, 273, 274, 275, 276, 277, 278, 279, 284, 285, - 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, - - 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, - 306, 307, 308, 309, 310, 315, 316, 317, 318, 319, - 320, 321, 322, 323, 328, 329, 330, 331, 332, 336, - 343, 349, 359, 366, 373, 381, 394, 399, 407 + 79, 84, 91, 98, 105, 112, 118, 122, 134, 138, + 143, 147, 157, 162, 163, 168, 171, 175, 177, 185, + 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, + 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, + 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, + 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, + 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, + 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, + 256, 257, 258, 259, 260, 261, 262, 263, 268, 269, + 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, + + 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, + 290, 291, 292, 293, 294, 299, 300, 301, 302, 303, + 304, 305, 306, 307, 312, 313, 314, 315, 316, 320, + 327, 333, 343, 350, 357, 365, 378, 383, 391 } ; /* The intent behind this definition is that it'll catch * any uses of REJECT which flex missed. */ #define REJECT reject_used_but_not_detected #define yymore() (yyg->yy_more_flag = 1) #define YY_MORE_ADJ yyg->yy_more_len #define YY_RESTORE_YY_MORE_OFFSET #line 1 "scanner.l" -/* - * Copyright 2012 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. - */ -#line 18 "scanner.l" +#line 2 "scanner.l" #include "ast.hpp" #define push_state(s) xhp_new_push_state(s, yyg) #define pop_state() xhp_new_pop_state(yyg) #define set_state(s) xhp_set_state(s, yyg) #define last_token() yyextra->last_token #define YY_USER_ACTION \ if (!yyg->yy_more_len) \ yyextra->first_lineno = yyextra->lineno; #define pttok(t, txt) \ yyextra->token_list.push_back(new xhpast::Token(t, txt, yyextra->list_size++)); \ *yylval = new xhpast::Node(0, yyextra->list_size - 1); #define ptok(t) \ pttok(t, yytext); #define tok(t) \ ptok(t); \ return yy_token(t, yyg) #define YY_USER_INIT \ if (yyextra->insert_token) { \ yyg->yy_init = 0; \ int ft = yyextra->insert_token; \ yyextra->insert_token = 0; \ return yy_token(ft, yyg); \ } using namespace std; const char* yytokname(int tok); static int yy_token(int tok, struct yyguts_t* yyg); static void yy_scan_newlines(const char* text, struct yyguts_t* yyg); /* PHP allows IF or if */ /* I think an interactive scanner is required because of the bison state * pushing we do. I'm putting an explicit interactive declaration here in case * someone tries adding -CF or whatever to the make flags. */ /* The different lexing states. Note that the transitions are done either * in the lex actions, or in a generic manner in yy_token(). */ -#line 3705 "scanner.lex.cpp" +#line 3690 "scanner.lex.cpp" #define INITIAL 0 #define PHP 1 #define PHP_COMMENT 2 #define PHP_EOL_COMMENT 3 #define PHP_DOC_COMMENT 4 #define PHP_HEREDOC_START 5 #define PHP_HEREDOC_NSTART 6 #define PHP_HEREDOC_NEWLINE 7 #define PHP_HEREDOC_DATA 8 #define PHP_NO_RESERVED_WORDS 9 #define PHP_NO_RESERVED_WORDS_PERSIST 10 #define PHP_ 11 #ifndef YY_NO_UNISTD_H /* Special case for "unistd.h", since it is non-ANSI. We include it way * down here because we want the user's section 1 to have been scanned first. * The user has a chance to override it with an option. */ /* %if-c-only */ #include /* %endif */ /* %if-c++-only */ /* %endif */ #endif #ifndef YY_EXTRA_TYPE #define YY_EXTRA_TYPE void * #endif /* %if-c-only Reentrant structure and macros (non-C++). */ /* %if-reentrant */ /* Holds the entire state of the reentrant scanner. */ struct yyguts_t { /* User-defined. Not touched by flex. */ YY_EXTRA_TYPE yyextra_r; /* The rest are the same as the globals declared in the non-reentrant scanner. */ FILE *yyin_r, *yyout_r; size_t yy_buffer_stack_top; /**< index of top of stack. */ size_t yy_buffer_stack_max; /**< capacity of stack. */ YY_BUFFER_STATE * yy_buffer_stack; /**< Stack as an array. */ char yy_hold_char; - yy_size_t yy_n_chars; - yy_size_t yyleng_r; + int yy_n_chars; + int yyleng_r; char *yy_c_buf_p; int yy_init; int yy_start; int yy_did_buffer_switch_on_eof; int yy_start_stack_ptr; int yy_start_stack_depth; int *yy_start_stack; yy_state_type yy_last_accepting_state; char* yy_last_accepting_cpos; int yylineno_r; int yy_flex_debug_r; char *yytext_r; int yy_more_flag; int yy_more_len; YYSTYPE * yylval_r; }; /* end struct yyguts_t */ /* %if-c-only */ static int yy_init_globals (yyscan_t yyscanner ); /* %endif */ /* %if-reentrant */ /* This must go here because YYSTYPE and YYLTYPE are included * from bison output in section 1.*/ # define yylval yyg->yylval_r int xhpastlex_init (yyscan_t* scanner); int xhpastlex_init_extra (YY_EXTRA_TYPE user_defined,yyscan_t* scanner); /* %endif */ /* %endif End reentrant structures and macros. */ /* Accessor methods to globals. These are made visible to non-reentrant scanners for convenience. */ int xhpastlex_destroy (yyscan_t yyscanner ); int xhpastget_debug (yyscan_t yyscanner ); void xhpastset_debug (int debug_flag ,yyscan_t yyscanner ); YY_EXTRA_TYPE xhpastget_extra (yyscan_t yyscanner ); void xhpastset_extra (YY_EXTRA_TYPE user_defined ,yyscan_t yyscanner ); FILE *xhpastget_in (yyscan_t yyscanner ); void xhpastset_in (FILE * in_str ,yyscan_t yyscanner ); FILE *xhpastget_out (yyscan_t yyscanner ); void xhpastset_out (FILE * out_str ,yyscan_t yyscanner ); -yy_size_t xhpastget_leng (yyscan_t yyscanner ); +int xhpastget_leng (yyscan_t yyscanner ); char *xhpastget_text (yyscan_t yyscanner ); int xhpastget_lineno (yyscan_t yyscanner ); void xhpastset_lineno (int line_number ,yyscan_t yyscanner ); /* %if-bison-bridge */ YYSTYPE * xhpastget_lval (yyscan_t yyscanner ); void xhpastset_lval (YYSTYPE * yylval_param ,yyscan_t yyscanner ); /* %endif */ /* Macros after this point can all be overridden by user definitions in * section 1. */ #ifndef YY_SKIP_YYWRAP #ifdef __cplusplus extern "C" int xhpastwrap (yyscan_t yyscanner ); #else extern int xhpastwrap (yyscan_t yyscanner ); #endif #endif /* %not-for-header */ static void yyunput (int c,char *buf_ptr ,yyscan_t yyscanner); /* %ok-for-header */ /* %endif */ #ifndef yytext_ptr static void yy_flex_strncpy (char *,yyconst char *,int ,yyscan_t yyscanner); #endif #ifdef YY_NEED_STRLEN static int yy_flex_strlen (yyconst char * ,yyscan_t yyscanner); #endif #ifndef YY_NO_INPUT /* %if-c-only Standard (non-C++) definition */ /* %not-for-header */ #ifdef __cplusplus static int yyinput (yyscan_t yyscanner ); #else static int input (yyscan_t yyscanner ); #endif /* %ok-for-header */ /* %endif */ #endif /* %if-c-only */ static void yy_push_state (int new_state ,yyscan_t yyscanner); static void yy_pop_state (yyscan_t yyscanner ); static int yy_top_state (yyscan_t yyscanner ); /* %endif */ /* Amount of stuff to slurp up with each read. */ #ifndef YY_READ_BUF_SIZE #define YY_READ_BUF_SIZE 8192 #endif /* Copy whatever the last rule matched to the standard output. */ #ifndef ECHO /* %if-c-only Standard (non-C++) definition */ /* This used to be an fputs(), but since the string might contain NUL's, * we now use fwrite(). */ #define ECHO fwrite( yytext, yyleng, 1, yyout ) /* %endif */ /* %if-c++-only C++ definition */ /* %endif */ #endif /* Gets input and stuffs it into "buf". number of characters read, or YY_NULL, * is returned in "result". */ #ifndef YY_INPUT #define YY_INPUT(buf,result,max_size) \ /* %% [5.0] fread()/read() definition of YY_INPUT goes here unless we're doing C++ \ */\ if ( YY_CURRENT_BUFFER_LVALUE->yy_is_interactive ) \ { \ int c = '*'; \ - yy_size_t n; \ + unsigned n; \ for ( n = 0; n < max_size && \ (c = getc( yyin )) != EOF && c != '\n'; ++n ) \ buf[n] = (char) c; \ if ( c == '\n' ) \ buf[n++] = (char) c; \ if ( c == EOF && ferror( yyin ) ) \ YY_FATAL_ERROR( "input in flex scanner failed" ); \ result = n; \ } \ else \ { \ errno=0; \ while ( (result = fread(buf, 1, max_size, yyin))==0 && ferror(yyin)) \ { \ if( errno != EINTR) \ { \ YY_FATAL_ERROR( "input in flex scanner failed" ); \ break; \ } \ errno=0; \ clearerr(yyin); \ } \ }\ \ /* %if-c++-only C++ definition \ */\ /* %endif */ #endif /* No semi-colon after return; correct usage is to write "yyterminate();" - * we don't want an extra ';' after the "return" because that will cause * some compilers to complain about unreachable statements. */ #ifndef yyterminate #define yyterminate() return YY_NULL #endif /* Number of entries by which start-condition stack grows. */ #ifndef YY_START_STACK_INCR #define YY_START_STACK_INCR 25 #endif /* Report a fatal error. */ #ifndef YY_FATAL_ERROR /* %if-c-only */ #define YY_FATAL_ERROR(msg) yy_fatal_error( msg , yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ #endif /* %if-tables-serialization structures and prototypes */ /* %not-for-header */ /* %ok-for-header */ /* %not-for-header */ /* %tables-yydmap generated elements */ /* %endif */ /* end tables serialization structures and prototypes */ /* %ok-for-header */ /* Default declaration of generated scanner - a define so the user can * easily add parameters. */ #ifndef YY_DECL #define YY_DECL_IS_OURS 1 /* %if-c-only Standard (non-C++) definition */ extern int xhpastlex \ (YYSTYPE * yylval_param ,yyscan_t yyscanner); #define YY_DECL int xhpastlex \ (YYSTYPE * yylval_param , yyscan_t yyscanner) /* %endif */ /* %if-c++-only C++ definition */ /* %endif */ #endif /* !YY_DECL */ /* Code executed at the beginning of each rule, after yytext and yyleng * have been set up. */ #ifndef YY_USER_ACTION #define YY_USER_ACTION #endif /* Code executed at the end of each rule. */ #ifndef YY_BREAK #define YY_BREAK break; #endif /* %% [6.0] YY_RULE_SETUP definition goes here */ #define YY_RULE_SETUP \ YY_USER_ACTION /* %not-for-header */ /** The main scanner function which does all the work. */ YY_DECL { register yy_state_type yy_current_state; register char *yy_cp, *yy_bp; register int yy_act; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* %% [7.0] user's declarations go here */ -#line 91 "scanner.l" +#line 75 "scanner.l" /* Open / close PHP + inline HTML */ -#line 4024 "scanner.lex.cpp" +#line 4009 "scanner.lex.cpp" yylval = yylval_param; if ( !yyg->yy_init ) { yyg->yy_init = 1; #ifdef YY_USER_INIT YY_USER_INIT; #endif if ( ! yyg->yy_start ) yyg->yy_start = 1; /* first start state */ if ( ! yyin ) /* %if-c-only */ yyin = stdin; /* %endif */ /* %if-c++-only */ /* %endif */ if ( ! yyout ) /* %if-c-only */ yyout = stdout; /* %endif */ /* %if-c++-only */ /* %endif */ if ( ! YY_CURRENT_BUFFER ) { xhpastensure_buffer_stack (yyscanner); YY_CURRENT_BUFFER_LVALUE = xhpast_create_buffer(yyin,YY_BUF_SIZE ,yyscanner); } xhpast_load_buffer_state(yyscanner ); } while ( 1 ) /* loops until end-of-file is reached */ { /* %% [8.0] yymore()-related code goes here */ yyg->yy_more_len = 0; if ( yyg->yy_more_flag ) { yyg->yy_more_len = yyg->yy_c_buf_p - yyg->yytext_ptr; yyg->yy_more_flag = 0; } yy_cp = yyg->yy_c_buf_p; /* Support of yytext. */ *yy_cp = yyg->yy_hold_char; /* yy_bp points to the position in yy_ch_buf of the start of * the current run. */ yy_bp = yy_cp; /* %% [9.0] code to set up and find next match goes here */ yy_current_state = yyg->yy_start; yy_match: do { register YY_CHAR yy_c = YY_SC_TO_UI(*yy_cp); if ( yy_accept[yy_current_state] ) { yyg->yy_last_accepting_state = yy_current_state; yyg->yy_last_accepting_cpos = yy_cp; } while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) { yy_current_state = (int) yy_def[yy_current_state]; } yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c]; ++yy_cp; } while ( yy_base[yy_current_state] != 12788 ); yy_find_action: /* %% [10.0] code to find the action number goes here */ yy_act = yy_accept[yy_current_state]; if ( yy_act == 0 ) { /* have to back up */ yy_cp = yyg->yy_last_accepting_cpos; yy_current_state = yyg->yy_last_accepting_state; yy_act = yy_accept[yy_current_state]; } YY_DO_BEFORE_ACTION; /* %% [11.0] code for yylineno update goes here */ do_action: /* This label is used only to access EOF actions. */ /* %% [12.0] debug code goes here */ if ( yy_flex_debug ) { if ( yy_act == 0 ) fprintf( stderr, "--scanner backing up\n" ); else if ( yy_act < 140 ) fprintf( stderr, "--accepting rule at line %ld (\"%s\")\n", (long)yy_rule_linenum[yy_act], yytext ); else if ( yy_act == 140 ) fprintf( stderr, "--accepting default rule (\"%s\")\n", yytext ); else if ( yy_act == 141 ) fprintf( stderr, "--(end of buffer or a NUL)\n" ); else fprintf( stderr, "--EOF (start condition %d)\n", YY_START ); } switch ( yy_act ) { /* beginning of action switch */ /* %% [13.0] actions go here */ case 0: /* must back up */ /* undo the effects of YY_DO_BEFORE_ACTION */ *yy_cp = yyg->yy_hold_char; yy_cp = yyg->yy_last_accepting_cpos; yy_current_state = yyg->yy_last_accepting_state; goto yy_find_action; case 1: /* rule 1 can match eol */ YY_RULE_SETUP -#line 95 "scanner.l" +#line 79 "scanner.l" { yy_scan_newlines(yytext + 5, yyg); // the state transition will be done in yy_token() tok(T_OPEN_TAG); } YY_BREAK case 2: YY_RULE_SETUP -#line 100 "scanner.l" +#line 84 "scanner.l" { if (yyextra->short_tags) { tok(T_OPEN_TAG); } else { tok(T_INLINE_HTML); } } YY_BREAK case 3: YY_RULE_SETUP -#line 107 "scanner.l" +#line 91 "scanner.l" { if (yyextra->short_tags) { tok(T_OPEN_TAG_WITH_ECHO); } else { tok(T_INLINE_HTML); } } YY_BREAK case 4: YY_RULE_SETUP -#line 114 "scanner.l" +#line 98 "scanner.l" { if (yyextra->asp_tags) { tok(T_OPEN_TAG); } else { tok(T_INLINE_HTML); } } YY_BREAK case 5: YY_RULE_SETUP -#line 121 "scanner.l" +#line 105 "scanner.l" { if (yyextra->asp_tags) { tok(T_OPEN_TAG_WITH_ECHO); } else { tok(T_INLINE_HTML); } } YY_BREAK case 6: /* rule 6 can match eol */ YY_RULE_SETUP -#line 128 "scanner.l" +#line 112 "scanner.l" { yy_scan_newlines(yytext, yyg); tok(T_INLINE_HTML); } YY_BREAK case 7: /* rule 7 can match eol */ YY_RULE_SETUP -#line 134 "scanner.l" +#line 118 "scanner.l" { yy_scan_newlines(yytext + 2, yyg); tok(T_CLOSE_TAG); } YY_BREAK case 8: YY_RULE_SETUP -#line 138 "scanner.l" +#line 122 "scanner.l" { if (yyextra->asp_tags) { tok(T_CLOSE_TAG); } else { yyless(1); tok(yytext[0]); } } YY_BREAK /* Comments and whitespace */ case 9: YY_RULE_SETUP -#line 150 "scanner.l" +#line 134 "scanner.l" { push_state(PHP_EOL_COMMENT); yymore(); } YY_BREAK case 10: /* rule 10 can match eol */ YY_RULE_SETUP -#line 154 "scanner.l" +#line 138 "scanner.l" { yy_scan_newlines(yytext + 3, yyg); push_state(PHP_DOC_COMMENT); yymore(); } YY_BREAK case 11: YY_RULE_SETUP -#line 159 "scanner.l" +#line 143 "scanner.l" { push_state(PHP_COMMENT); yymore(); } YY_BREAK case 12: /* rule 12 can match eol */ YY_RULE_SETUP -#line 163 "scanner.l" +#line 147 "scanner.l" { yy_scan_newlines(yytext, yyg); ptok(T_WHITESPACE); } YY_BREAK case YY_STATE_EOF(PHP_EOL_COMMENT): -#line 168 "scanner.l" +#line 152 "scanner.l" { ptok(T_COMMENT); pop_state(); } YY_BREAK case 13: /* rule 13 can match eol */ YY_RULE_SETUP -#line 173 "scanner.l" +#line 157 "scanner.l" { ++yyextra->lineno; ptok(T_COMMENT); pop_state(); } YY_BREAK case 14: YY_RULE_SETUP -#line 178 "scanner.l" +#line 162 "scanner.l" yymore(); YY_BREAK case 15: YY_RULE_SETUP -#line 179 "scanner.l" +#line 163 "scanner.l" { yyless(yyleng - 2); ptok(T_COMMENT); pop_state(); } YY_BREAK case 16: YY_RULE_SETUP -#line 184 "scanner.l" +#line 168 "scanner.l" yymore(); YY_BREAK case 17: /* rule 17 can match eol */ YY_RULE_SETUP -#line 187 "scanner.l" +#line 171 "scanner.l" { ++yyextra->lineno; yymore(); } YY_BREAK case 18: YY_RULE_SETUP -#line 191 "scanner.l" +#line 175 "scanner.l" yymore(); YY_BREAK case 19: YY_RULE_SETUP -#line 193 "scanner.l" +#line 177 "scanner.l" { ptok(T_DOC_COMMENT); pop_state(); } YY_BREAK case YY_STATE_EOF(PHP_DOC_COMMENT): -#line 197 "scanner.l" +#line 181 "scanner.l" { ptok(T_DOC_COMMENT); pop_state(); } YY_BREAK case 20: YY_RULE_SETUP -#line 201 "scanner.l" +#line 185 "scanner.l" { ptok(T_COMMENT); pop_state(); } YY_BREAK case YY_STATE_EOF(PHP_COMMENT): -#line 205 "scanner.l" +#line 189 "scanner.l" { ptok(T_COMMENT); pop_state(); } YY_BREAK /* Reserved words */ case 21: YY_RULE_SETUP -#line 212 "scanner.l" +#line 196 "scanner.l" tok(T_INCLUDE); YY_BREAK case 22: YY_RULE_SETUP -#line 213 "scanner.l" +#line 197 "scanner.l" tok(T_INCLUDE_ONCE); YY_BREAK case 23: YY_RULE_SETUP -#line 214 "scanner.l" +#line 198 "scanner.l" tok(T_EVAL); YY_BREAK case 24: YY_RULE_SETUP -#line 215 "scanner.l" +#line 199 "scanner.l" tok(T_REQUIRE); YY_BREAK case 25: YY_RULE_SETUP -#line 216 "scanner.l" +#line 200 "scanner.l" tok(T_REQUIRE_ONCE); YY_BREAK case 26: YY_RULE_SETUP -#line 217 "scanner.l" +#line 201 "scanner.l" tok(T_LOGICAL_OR); YY_BREAK case 27: YY_RULE_SETUP -#line 218 "scanner.l" +#line 202 "scanner.l" tok(T_LOGICAL_XOR); YY_BREAK case 28: YY_RULE_SETUP -#line 219 "scanner.l" +#line 203 "scanner.l" tok(T_LOGICAL_AND); YY_BREAK case 29: YY_RULE_SETUP -#line 220 "scanner.l" +#line 204 "scanner.l" tok(T_PRINT); YY_BREAK case 30: YY_RULE_SETUP -#line 221 "scanner.l" +#line 205 "scanner.l" tok(T_INSTANCEOF); YY_BREAK case 31: YY_RULE_SETUP -#line 222 "scanner.l" +#line 206 "scanner.l" tok(T_NEW); YY_BREAK case 32: YY_RULE_SETUP -#line 223 "scanner.l" +#line 207 "scanner.l" tok(T_CLONE); YY_BREAK case 33: YY_RULE_SETUP -#line 224 "scanner.l" +#line 208 "scanner.l" tok(T_EXIT); YY_BREAK case 34: YY_RULE_SETUP -#line 225 "scanner.l" +#line 209 "scanner.l" tok(T_IF); YY_BREAK case 35: YY_RULE_SETUP -#line 226 "scanner.l" +#line 210 "scanner.l" tok(T_ELSEIF); YY_BREAK case 36: YY_RULE_SETUP -#line 227 "scanner.l" +#line 211 "scanner.l" tok(T_ELSE); YY_BREAK case 37: YY_RULE_SETUP -#line 228 "scanner.l" +#line 212 "scanner.l" tok(T_ENDIF); YY_BREAK case 38: YY_RULE_SETUP -#line 229 "scanner.l" +#line 213 "scanner.l" tok(T_ECHO); YY_BREAK case 39: YY_RULE_SETUP -#line 230 "scanner.l" +#line 214 "scanner.l" tok(T_DO); YY_BREAK case 40: YY_RULE_SETUP -#line 231 "scanner.l" +#line 215 "scanner.l" tok(T_WHILE); YY_BREAK case 41: YY_RULE_SETUP -#line 232 "scanner.l" +#line 216 "scanner.l" tok(T_ENDWHILE); YY_BREAK case 42: YY_RULE_SETUP -#line 233 "scanner.l" +#line 217 "scanner.l" tok(T_FOR); YY_BREAK case 43: YY_RULE_SETUP -#line 234 "scanner.l" +#line 218 "scanner.l" tok(T_ENDFOR); YY_BREAK case 44: YY_RULE_SETUP -#line 235 "scanner.l" +#line 219 "scanner.l" tok(T_FOREACH); YY_BREAK case 45: YY_RULE_SETUP -#line 236 "scanner.l" +#line 220 "scanner.l" tok(T_ENDFOREACH); YY_BREAK case 46: YY_RULE_SETUP -#line 237 "scanner.l" +#line 221 "scanner.l" tok(T_DECLARE); YY_BREAK case 47: YY_RULE_SETUP -#line 238 "scanner.l" +#line 222 "scanner.l" tok(T_ENDDECLARE); YY_BREAK case 48: YY_RULE_SETUP -#line 239 "scanner.l" +#line 223 "scanner.l" tok(T_AS); YY_BREAK case 49: YY_RULE_SETUP -#line 240 "scanner.l" +#line 224 "scanner.l" tok(T_SWITCH); YY_BREAK case 50: YY_RULE_SETUP -#line 241 "scanner.l" +#line 225 "scanner.l" tok(T_ENDSWITCH); YY_BREAK case 51: YY_RULE_SETUP -#line 242 "scanner.l" +#line 226 "scanner.l" tok(T_CASE); YY_BREAK case 52: YY_RULE_SETUP -#line 243 "scanner.l" +#line 227 "scanner.l" tok(T_DEFAULT); YY_BREAK case 53: YY_RULE_SETUP -#line 244 "scanner.l" +#line 228 "scanner.l" tok(T_BREAK); YY_BREAK case 54: YY_RULE_SETUP -#line 245 "scanner.l" +#line 229 "scanner.l" tok(T_CONTINUE); YY_BREAK case 55: YY_RULE_SETUP -#line 246 "scanner.l" +#line 230 "scanner.l" tok(T_GOTO); YY_BREAK case 56: YY_RULE_SETUP -#line 247 "scanner.l" +#line 231 "scanner.l" tok(T_FUNCTION); YY_BREAK case 57: YY_RULE_SETUP -#line 248 "scanner.l" +#line 232 "scanner.l" tok(T_CONST); YY_BREAK case 58: YY_RULE_SETUP -#line 249 "scanner.l" +#line 233 "scanner.l" tok(T_RETURN); YY_BREAK case 59: YY_RULE_SETUP -#line 250 "scanner.l" +#line 234 "scanner.l" tok(T_TRY); YY_BREAK case 60: YY_RULE_SETUP -#line 251 "scanner.l" +#line 235 "scanner.l" tok(T_CATCH); YY_BREAK case 61: YY_RULE_SETUP -#line 252 "scanner.l" +#line 236 "scanner.l" tok(T_THROW); YY_BREAK case 62: YY_RULE_SETUP -#line 253 "scanner.l" +#line 237 "scanner.l" tok(T_USE); YY_BREAK case 63: YY_RULE_SETUP -#line 254 "scanner.l" +#line 238 "scanner.l" tok(T_GLOBAL); YY_BREAK case 64: YY_RULE_SETUP -#line 255 "scanner.l" +#line 239 "scanner.l" tok(T_STATIC); YY_BREAK case 65: YY_RULE_SETUP -#line 256 "scanner.l" +#line 240 "scanner.l" tok(T_ABSTRACT); YY_BREAK case 66: YY_RULE_SETUP -#line 257 "scanner.l" +#line 241 "scanner.l" tok(T_FINAL); YY_BREAK case 67: YY_RULE_SETUP -#line 258 "scanner.l" +#line 242 "scanner.l" tok(T_PRIVATE); YY_BREAK case 68: YY_RULE_SETUP -#line 259 "scanner.l" +#line 243 "scanner.l" tok(T_PROTECTED); YY_BREAK case 69: YY_RULE_SETUP -#line 260 "scanner.l" +#line 244 "scanner.l" tok(T_PUBLIC); YY_BREAK case 70: YY_RULE_SETUP -#line 261 "scanner.l" +#line 245 "scanner.l" tok(T_VAR); YY_BREAK case 71: YY_RULE_SETUP -#line 262 "scanner.l" +#line 246 "scanner.l" tok(T_UNSET); YY_BREAK case 72: YY_RULE_SETUP -#line 263 "scanner.l" +#line 247 "scanner.l" tok(T_ISSET); YY_BREAK case 73: YY_RULE_SETUP -#line 264 "scanner.l" +#line 248 "scanner.l" tok(T_EMPTY); YY_BREAK case 74: YY_RULE_SETUP -#line 265 "scanner.l" +#line 249 "scanner.l" tok(T_HALT_COMPILER); YY_BREAK case 75: YY_RULE_SETUP -#line 266 "scanner.l" +#line 250 "scanner.l" tok(T_CLASS); YY_BREAK case 76: YY_RULE_SETUP -#line 267 "scanner.l" +#line 251 "scanner.l" tok(T_INTERFACE); YY_BREAK case 77: YY_RULE_SETUP -#line 268 "scanner.l" +#line 252 "scanner.l" tok(T_EXTENDS); YY_BREAK case 78: YY_RULE_SETUP -#line 269 "scanner.l" +#line 253 "scanner.l" tok(T_IMPLEMENTS); YY_BREAK case 79: YY_RULE_SETUP -#line 270 "scanner.l" +#line 254 "scanner.l" tok(T_LIST); YY_BREAK case 80: YY_RULE_SETUP -#line 271 "scanner.l" +#line 255 "scanner.l" tok(T_ARRAY); YY_BREAK case 81: YY_RULE_SETUP -#line 272 "scanner.l" +#line 256 "scanner.l" tok(T_CLASS_C); YY_BREAK case 82: YY_RULE_SETUP -#line 273 "scanner.l" +#line 257 "scanner.l" tok(T_METHOD_C); YY_BREAK case 83: YY_RULE_SETUP -#line 274 "scanner.l" +#line 258 "scanner.l" tok(T_FUNC_C); YY_BREAK case 84: YY_RULE_SETUP -#line 275 "scanner.l" +#line 259 "scanner.l" tok(T_LINE); YY_BREAK case 85: YY_RULE_SETUP -#line 276 "scanner.l" +#line 260 "scanner.l" tok(T_FILE); YY_BREAK case 86: YY_RULE_SETUP -#line 277 "scanner.l" +#line 261 "scanner.l" tok(T_NAMESPACE); YY_BREAK case 87: YY_RULE_SETUP -#line 278 "scanner.l" +#line 262 "scanner.l" tok(T_NS_C); YY_BREAK case 88: YY_RULE_SETUP -#line 279 "scanner.l" +#line 263 "scanner.l" tok(T_DIR); YY_BREAK /* Operators */ case 89: YY_RULE_SETUP -#line 284 "scanner.l" +#line 268 "scanner.l" tok(T_PLUS_EQUAL); YY_BREAK case 90: YY_RULE_SETUP -#line 285 "scanner.l" +#line 269 "scanner.l" tok(T_MINUS_EQUAL); YY_BREAK case 91: YY_RULE_SETUP -#line 286 "scanner.l" +#line 270 "scanner.l" tok(T_MUL_EQUAL); YY_BREAK case 92: YY_RULE_SETUP -#line 287 "scanner.l" +#line 271 "scanner.l" tok(T_DIV_EQUAL); YY_BREAK case 93: YY_RULE_SETUP -#line 288 "scanner.l" +#line 272 "scanner.l" tok(T_CONCAT_EQUAL); YY_BREAK case 94: YY_RULE_SETUP -#line 289 "scanner.l" +#line 273 "scanner.l" tok(T_MOD_EQUAL); YY_BREAK case 95: YY_RULE_SETUP -#line 290 "scanner.l" +#line 274 "scanner.l" tok(T_AND_EQUAL); YY_BREAK case 96: YY_RULE_SETUP -#line 291 "scanner.l" +#line 275 "scanner.l" tok(T_OR_EQUAL); YY_BREAK case 97: YY_RULE_SETUP -#line 292 "scanner.l" +#line 276 "scanner.l" tok(T_XOR_EQUAL); YY_BREAK case 98: YY_RULE_SETUP -#line 293 "scanner.l" +#line 277 "scanner.l" tok(T_SL_EQUAL); YY_BREAK case 99: YY_RULE_SETUP -#line 294 "scanner.l" +#line 278 "scanner.l" tok(T_SR_EQUAL); YY_BREAK case 100: YY_RULE_SETUP -#line 295 "scanner.l" +#line 279 "scanner.l" tok(T_BOOLEAN_OR); YY_BREAK case 101: YY_RULE_SETUP -#line 296 "scanner.l" +#line 280 "scanner.l" tok(T_BOOLEAN_AND); YY_BREAK case 102: YY_RULE_SETUP -#line 297 "scanner.l" +#line 281 "scanner.l" tok(T_IS_EQUAL); YY_BREAK case 103: YY_RULE_SETUP -#line 298 "scanner.l" +#line 282 "scanner.l" tok(T_IS_NOT_EQUAL); YY_BREAK case 104: YY_RULE_SETUP -#line 299 "scanner.l" +#line 283 "scanner.l" tok(T_IS_IDENTICAL); YY_BREAK case 105: YY_RULE_SETUP -#line 300 "scanner.l" +#line 284 "scanner.l" tok(T_IS_NOT_IDENTICAL); YY_BREAK case 106: YY_RULE_SETUP -#line 301 "scanner.l" +#line 285 "scanner.l" tok(T_IS_SMALLER_OR_EQUAL); YY_BREAK case 107: YY_RULE_SETUP -#line 302 "scanner.l" +#line 286 "scanner.l" tok(T_IS_GREATER_OR_EQUAL); YY_BREAK case 108: YY_RULE_SETUP -#line 303 "scanner.l" +#line 287 "scanner.l" tok(T_SL); YY_BREAK case 109: YY_RULE_SETUP -#line 304 "scanner.l" +#line 288 "scanner.l" tok(T_SR); YY_BREAK case 110: YY_RULE_SETUP -#line 305 "scanner.l" +#line 289 "scanner.l" tok(T_INC); YY_BREAK case 111: YY_RULE_SETUP -#line 306 "scanner.l" +#line 290 "scanner.l" tok(T_DEC); YY_BREAK case 112: YY_RULE_SETUP -#line 307 "scanner.l" +#line 291 "scanner.l" tok(T_OBJECT_OPERATOR); YY_BREAK case 113: YY_RULE_SETUP -#line 308 "scanner.l" +#line 292 "scanner.l" tok(T_DOUBLE_ARROW); YY_BREAK case 114: YY_RULE_SETUP -#line 309 "scanner.l" +#line 293 "scanner.l" tok(T_PAAMAYIM_NEKUDOTAYIM); YY_BREAK case 115: YY_RULE_SETUP -#line 310 "scanner.l" +#line 294 "scanner.l" tok(T_NS_SEPARATOR); YY_BREAK /* Casts */ case 116: YY_RULE_SETUP -#line 315 "scanner.l" +#line 299 "scanner.l" tok(T_INT_CAST); YY_BREAK case 117: YY_RULE_SETUP -#line 316 "scanner.l" +#line 300 "scanner.l" tok(T_DOUBLE_CAST); YY_BREAK case 118: YY_RULE_SETUP -#line 317 "scanner.l" +#line 301 "scanner.l" tok(T_STRING_CAST); YY_BREAK case 119: YY_RULE_SETUP -#line 318 "scanner.l" +#line 302 "scanner.l" tok(T_UNICODE_CAST); YY_BREAK case 120: YY_RULE_SETUP -#line 319 "scanner.l" +#line 303 "scanner.l" tok(T_BINARY_CAST); YY_BREAK case 121: YY_RULE_SETUP -#line 320 "scanner.l" +#line 304 "scanner.l" tok(T_ARRAY_CAST); YY_BREAK case 122: YY_RULE_SETUP -#line 321 "scanner.l" +#line 305 "scanner.l" tok(T_OBJECT_CAST); YY_BREAK case 123: YY_RULE_SETUP -#line 322 "scanner.l" +#line 306 "scanner.l" tok(T_BOOL_CAST); YY_BREAK case 124: YY_RULE_SETUP -#line 323 "scanner.l" +#line 307 "scanner.l" tok(T_UNSET_CAST); YY_BREAK /* Scalars (parsing these doesn't really matter since we just pass them through literally) */ case 125: YY_RULE_SETUP -#line 328 "scanner.l" +#line 312 "scanner.l" tok(T_LNUMBER); YY_BREAK case 126: YY_RULE_SETUP -#line 329 "scanner.l" +#line 313 "scanner.l" tok(T_DNUMBER); YY_BREAK case 127: YY_RULE_SETUP -#line 330 "scanner.l" +#line 314 "scanner.l" tok(T_STRING); YY_BREAK case 128: YY_RULE_SETUP -#line 331 "scanner.l" +#line 315 "scanner.l" tok(T_VARIABLE); YY_BREAK case 129: /* rule 129 can match eol */ YY_RULE_SETUP -#line 332 "scanner.l" +#line 316 "scanner.l" { yy_scan_newlines(yytext, yyg); tok(T_CONSTANT_ENCAPSED_STRING); } YY_BREAK case 130: /* rule 130 can match eol */ YY_RULE_SETUP -#line 336 "scanner.l" +#line 320 "scanner.l" { yy_scan_newlines(yytext, yyg); tok(T_BACKTICKS_EXPR); } YY_BREAK /* (HERE|NOW)DOC's */ case 131: YY_RULE_SETUP -#line 343 "scanner.l" +#line 327 "scanner.l" { push_state(PHP_HEREDOC_START); yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 132: YY_RULE_SETUP -#line 349 "scanner.l" +#line 333 "scanner.l" { // Create a new string for the heredoc label. Since we're using yymore above // yytext will actually start at the "<<<" and not the label. Use of // heredoc_yyleng jumps past that. Then we add 1 to get past the " or '. The // match is similar to calculate length. yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng + 1, yyleng - yyextra->heredoc_yyleng - 2); set_state(PHP_HEREDOC_NSTART); yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 133: YY_RULE_SETUP -#line 359 "scanner.l" +#line 343 "scanner.l" { yyextra->heredoc_label = string(yytext + yyextra->heredoc_yyleng); set_state(PHP_HEREDOC_NSTART); yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 134: /* rule 134 can match eol */ YY_RULE_SETUP -#line 366 "scanner.l" +#line 350 "scanner.l" { ++yyextra->lineno; yyextra->heredoc_data = yytext + yyleng; set_state(PHP_HEREDOC_DATA); yymore(); } YY_BREAK case 135: /* rule 135 can match eol */ YY_RULE_SETUP -#line 373 "scanner.l" +#line 357 "scanner.l" { ++yyextra->lineno; set_state(PHP_HEREDOC_NEWLINE); yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 136: /* rule 136 can match eol */ YY_RULE_SETUP -#line 381 "scanner.l" +#line 365 "scanner.l" { if (strncmp(yyextra->heredoc_label.c_str(), yytext + yyextra->heredoc_yyleng, yyextra->heredoc_label.size()) == 0) { switch (yytext[yyextra->heredoc_yyleng + yyextra->heredoc_label.size()]) { case ';': case '\n': case '\r': yyless(yyleng - (yyleng - yyextra->heredoc_yyleng - yyextra->heredoc_label.size())); pop_state(); tok(T_HEREDOC); } } ++yyextra->lineno; yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 137: YY_RULE_SETUP -#line 394 "scanner.l" +#line 378 "scanner.l" { set_state(PHP_HEREDOC_DATA); yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK case 138: /* rule 138 can match eol */ YY_RULE_SETUP -#line 399 "scanner.l" +#line 383 "scanner.l" { ++yyextra->lineno; yyextra->heredoc_yyleng = yyleng; yymore(); } YY_BREAK /* Other */ case 139: /* rule 139 can match eol */ YY_RULE_SETUP -#line 407 "scanner.l" +#line 391 "scanner.l" { tok(yytext[0]); // fix unused function warnings yy_top_state(NULL); yyunput(0, 0, NULL); } YY_BREAK case 140: YY_RULE_SETUP -#line 414 "scanner.l" +#line 398 "scanner.l" YY_FATAL_ERROR( "flex scanner jammed" ); YY_BREAK -#line 5039 "scanner.lex.cpp" +#line 5024 "scanner.lex.cpp" case YY_STATE_EOF(INITIAL): case YY_STATE_EOF(PHP): case YY_STATE_EOF(PHP_HEREDOC_START): case YY_STATE_EOF(PHP_HEREDOC_NSTART): case YY_STATE_EOF(PHP_HEREDOC_NEWLINE): case YY_STATE_EOF(PHP_HEREDOC_DATA): case YY_STATE_EOF(PHP_NO_RESERVED_WORDS): case YY_STATE_EOF(PHP_NO_RESERVED_WORDS_PERSIST): case YY_STATE_EOF(PHP_): yyterminate(); case YY_END_OF_BUFFER: { /* Amount of text matched not including the EOB char. */ int yy_amount_of_matched_text = (int) (yy_cp - yyg->yytext_ptr) - 1; /* Undo the effects of YY_DO_BEFORE_ACTION. */ *yy_cp = yyg->yy_hold_char; YY_RESTORE_YY_MORE_OFFSET if ( YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_NEW ) { /* We're scanning a new file or input source. It's * possible that this happened because the user * just pointed yyin at a new source and called * xhpastlex(). If so, then we have to assure * consistency between YY_CURRENT_BUFFER and our * globals. Here is the right place to do so, because * this is the first action (other than possibly a * back-up) that will match for the new input source. */ yyg->yy_n_chars = YY_CURRENT_BUFFER_LVALUE->yy_n_chars; YY_CURRENT_BUFFER_LVALUE->yy_input_file = yyin; YY_CURRENT_BUFFER_LVALUE->yy_buffer_status = YY_BUFFER_NORMAL; } /* Note that here we test for yy_c_buf_p "<=" to the position * of the first EOB in the buffer, since yy_c_buf_p will * already have been incremented past the NUL character * (since all states make transitions on EOB to the * end-of-buffer state). Contrast this with the test * in input(). */ if ( yyg->yy_c_buf_p <= &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] ) { /* This was really a NUL. */ yy_state_type yy_next_state; yyg->yy_c_buf_p = yyg->yytext_ptr + yy_amount_of_matched_text; yy_current_state = yy_get_previous_state( yyscanner ); /* Okay, we're now positioned to make the NUL * transition. We couldn't have * yy_get_previous_state() go ahead and do it * for us because it doesn't know how to deal * with the possibility of jamming (and we don't * want to build jamming into it because then it * will run more slowly). */ yy_next_state = yy_try_NUL_trans( yy_current_state , yyscanner); yy_bp = yyg->yytext_ptr + YY_MORE_ADJ; if ( yy_next_state ) { /* Consume the NUL. */ yy_cp = ++yyg->yy_c_buf_p; yy_current_state = yy_next_state; goto yy_match; } else { /* %% [14.0] code to do back-up for compressed tables and set up yy_cp goes here */ yy_cp = yyg->yy_c_buf_p; goto yy_find_action; } } else switch ( yy_get_next_buffer( yyscanner ) ) { case EOB_ACT_END_OF_FILE: { yyg->yy_did_buffer_switch_on_eof = 0; if ( xhpastwrap(yyscanner ) ) { /* Note: because we've taken care in * yy_get_next_buffer() to have set up * yytext, we can now set up * yy_c_buf_p so that if some total * hoser (like flex itself) wants to * call the scanner after we return the * YY_NULL, it'll still work - another * YY_NULL will get returned. */ yyg->yy_c_buf_p = yyg->yytext_ptr + YY_MORE_ADJ; yy_act = YY_STATE_EOF(YY_START); goto do_action; } else { if ( ! yyg->yy_did_buffer_switch_on_eof ) YY_NEW_FILE; } break; } case EOB_ACT_CONTINUE_SCAN: yyg->yy_c_buf_p = yyg->yytext_ptr + yy_amount_of_matched_text; yy_current_state = yy_get_previous_state( yyscanner ); yy_cp = yyg->yy_c_buf_p; yy_bp = yyg->yytext_ptr + YY_MORE_ADJ; goto yy_match; case EOB_ACT_LAST_MATCH: yyg->yy_c_buf_p = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars]; yy_current_state = yy_get_previous_state( yyscanner ); yy_cp = yyg->yy_c_buf_p; yy_bp = yyg->yytext_ptr + YY_MORE_ADJ; goto yy_find_action; } break; } default: YY_FATAL_ERROR( "fatal flex scanner internal error--no action found" ); } /* end of action switch */ } /* end of scanning one token */ } /* end of xhpastlex */ /* %ok-for-header */ /* %if-c++-only */ /* %not-for-header */ /* %ok-for-header */ /* %endif */ /* yy_get_next_buffer - try to read in a new buffer * * Returns a code representing an action: * EOB_ACT_LAST_MATCH - * EOB_ACT_CONTINUE_SCAN - continue scanning from current position * EOB_ACT_END_OF_FILE - end of file */ /* %if-c-only */ static int yy_get_next_buffer (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; register char *dest = YY_CURRENT_BUFFER_LVALUE->yy_ch_buf; register char *source = yyg->yytext_ptr; register int number_to_move, i; int ret_val; if ( yyg->yy_c_buf_p > &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars + 1] ) YY_FATAL_ERROR( "fatal flex scanner internal error--end of buffer missed" ); if ( YY_CURRENT_BUFFER_LVALUE->yy_fill_buffer == 0 ) { /* Don't try to fill the buffer, so this is an EOF. */ if ( yyg->yy_c_buf_p - yyg->yytext_ptr - YY_MORE_ADJ == 1 ) { /* We matched a single character, the EOB, so * treat this as a final EOF. */ return EOB_ACT_END_OF_FILE; } else { /* We matched some text prior to the EOB, first * process it. */ return EOB_ACT_LAST_MATCH; } } /* Try to read more data. */ /* First move last chars to start of buffer. */ number_to_move = (int) (yyg->yy_c_buf_p - yyg->yytext_ptr) - 1; for ( i = 0; i < number_to_move; ++i ) *(dest++) = *(source++); if ( YY_CURRENT_BUFFER_LVALUE->yy_buffer_status == YY_BUFFER_EOF_PENDING ) /* don't do the read, it's not guaranteed to return an EOF, * just force an EOF */ YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars = 0; else { - yy_size_t num_to_read = + int num_to_read = YY_CURRENT_BUFFER_LVALUE->yy_buf_size - number_to_move - 1; while ( num_to_read <= 0 ) { /* Not enough room in the buffer - grow it. */ /* just a shorter name for the current buffer */ YY_BUFFER_STATE b = YY_CURRENT_BUFFER; int yy_c_buf_p_offset = (int) (yyg->yy_c_buf_p - b->yy_ch_buf); if ( b->yy_is_our_buffer ) { - yy_size_t new_size = b->yy_buf_size * 2; + int new_size = b->yy_buf_size * 2; if ( new_size <= 0 ) b->yy_buf_size += b->yy_buf_size / 8; else b->yy_buf_size *= 2; b->yy_ch_buf = (char *) /* Include room in for 2 EOB chars. */ xhpastrealloc((void *) b->yy_ch_buf,b->yy_buf_size + 2 ,yyscanner ); } else /* Can't grow it, we don't own it. */ b->yy_ch_buf = 0; if ( ! b->yy_ch_buf ) YY_FATAL_ERROR( "fatal error - scanner input buffer overflow" ); yyg->yy_c_buf_p = &b->yy_ch_buf[yy_c_buf_p_offset]; num_to_read = YY_CURRENT_BUFFER_LVALUE->yy_buf_size - number_to_move - 1; } if ( num_to_read > YY_READ_BUF_SIZE ) num_to_read = YY_READ_BUF_SIZE; /* Read in more data. */ YY_INPUT( (&YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[number_to_move]), - yyg->yy_n_chars, num_to_read ); + yyg->yy_n_chars, (size_t) num_to_read ); YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars; } if ( yyg->yy_n_chars == 0 ) { if ( number_to_move == YY_MORE_ADJ ) { ret_val = EOB_ACT_END_OF_FILE; xhpastrestart(yyin ,yyscanner); } else { ret_val = EOB_ACT_LAST_MATCH; YY_CURRENT_BUFFER_LVALUE->yy_buffer_status = YY_BUFFER_EOF_PENDING; } } else ret_val = EOB_ACT_CONTINUE_SCAN; if ((yy_size_t) (yyg->yy_n_chars + number_to_move) > YY_CURRENT_BUFFER_LVALUE->yy_buf_size) { /* Extend the array by 50%, plus the number we really need. */ yy_size_t new_size = yyg->yy_n_chars + number_to_move + (yyg->yy_n_chars >> 1); YY_CURRENT_BUFFER_LVALUE->yy_ch_buf = (char *) xhpastrealloc((void *) YY_CURRENT_BUFFER_LVALUE->yy_ch_buf,new_size ,yyscanner ); if ( ! YY_CURRENT_BUFFER_LVALUE->yy_ch_buf ) YY_FATAL_ERROR( "out of dynamic memory in yy_get_next_buffer()" ); } yyg->yy_n_chars += number_to_move; YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] = YY_END_OF_BUFFER_CHAR; YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars + 1] = YY_END_OF_BUFFER_CHAR; yyg->yytext_ptr = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[0]; return ret_val; } /* yy_get_previous_state - get the state just before the EOB char was reached */ /* %if-c-only */ /* %not-for-header */ static yy_state_type yy_get_previous_state (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { register yy_state_type yy_current_state; register char *yy_cp; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* %% [15.0] code to get the start state into yy_current_state goes here */ yy_current_state = yyg->yy_start; for ( yy_cp = yyg->yytext_ptr + YY_MORE_ADJ; yy_cp < yyg->yy_c_buf_p; ++yy_cp ) { /* %% [16.0] code to find the next state goes here */ if ( yy_accept[yy_current_state] ) { yyg->yy_last_accepting_state = yy_current_state; yyg->yy_last_accepting_cpos = yy_cp; } if ( *yy_cp ) { register YY_CHAR yy_c = YY_SC_TO_UI(*yy_cp); if ( yy_accept[yy_current_state] ) { yyg->yy_last_accepting_state = yy_current_state; yyg->yy_last_accepting_cpos = yy_cp; } while ( yy_chk[yy_base[yy_current_state] + yy_c] != yy_current_state ) { yy_current_state = (int) yy_def[yy_current_state]; } yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c]; } else yy_current_state = yy_NUL_trans[yy_current_state]; } return yy_current_state; } /* yy_try_NUL_trans - try to make a transition on the NUL character * * synopsis * next_state = yy_try_NUL_trans( current_state ); */ /* %if-c-only */ static yy_state_type yy_try_NUL_trans (yy_state_type yy_current_state , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { register int yy_is_jam; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* This var may be unused depending upon options. */ /* %% [17.0] code to find the next state, and perhaps do backing up, goes here */ yy_current_state = yy_NUL_trans[yy_current_state]; yy_is_jam = (yy_current_state == 0); return yy_is_jam ? 0 : yy_current_state; } /* %if-c-only */ static void yyunput (int c, register char * yy_bp , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { register char *yy_cp; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yy_cp = yyg->yy_c_buf_p; /* undo effects of setting up yytext */ *yy_cp = yyg->yy_hold_char; if ( yy_cp < YY_CURRENT_BUFFER_LVALUE->yy_ch_buf + 2 ) { /* need to shift things up to make room */ /* +2 for EOB chars. */ - register yy_size_t number_to_move = yyg->yy_n_chars + 2; + register int number_to_move = yyg->yy_n_chars + 2; register char *dest = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[ YY_CURRENT_BUFFER_LVALUE->yy_buf_size + 2]; register char *source = &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[number_to_move]; while ( source > YY_CURRENT_BUFFER_LVALUE->yy_ch_buf ) *--dest = *--source; yy_cp += (int) (dest - source); yy_bp += (int) (dest - source); YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars = YY_CURRENT_BUFFER_LVALUE->yy_buf_size; if ( yy_cp < YY_CURRENT_BUFFER_LVALUE->yy_ch_buf + 2 ) YY_FATAL_ERROR( "flex scanner push-back overflow" ); } *--yy_cp = (char) c; /* %% [18.0] update yylineno here */ yyg->yytext_ptr = yy_bp; yyg->yy_hold_char = *yy_cp; yyg->yy_c_buf_p = yy_cp; } /* %if-c-only */ /* %endif */ /* %if-c-only */ #ifndef YY_NO_INPUT #ifdef __cplusplus static int yyinput (yyscan_t yyscanner) #else static int input (yyscan_t yyscanner) #endif /* %endif */ /* %if-c++-only */ /* %endif */ { int c; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; *yyg->yy_c_buf_p = yyg->yy_hold_char; if ( *yyg->yy_c_buf_p == YY_END_OF_BUFFER_CHAR ) { /* yy_c_buf_p now points to the character we want to return. * If this occurs *before* the EOB characters, then it's a * valid NUL; if not, then we've hit the end of the buffer. */ if ( yyg->yy_c_buf_p < &YY_CURRENT_BUFFER_LVALUE->yy_ch_buf[yyg->yy_n_chars] ) /* This was really a NUL. */ *yyg->yy_c_buf_p = '\0'; else { /* need more input */ - yy_size_t offset = yyg->yy_c_buf_p - yyg->yytext_ptr; + int offset = yyg->yy_c_buf_p - yyg->yytext_ptr; ++yyg->yy_c_buf_p; switch ( yy_get_next_buffer( yyscanner ) ) { case EOB_ACT_LAST_MATCH: /* This happens because yy_g_n_b() * sees that we've accumulated a * token and flags that we need to * try matching the token before * proceeding. But for input(), * there's no matching to consider. * So convert the EOB_ACT_LAST_MATCH * to EOB_ACT_END_OF_FILE. */ /* Reset buffer status. */ xhpastrestart(yyin ,yyscanner); /*FALLTHROUGH*/ case EOB_ACT_END_OF_FILE: { if ( xhpastwrap(yyscanner ) ) - return 0; + return EOF; if ( ! yyg->yy_did_buffer_switch_on_eof ) YY_NEW_FILE; #ifdef __cplusplus return yyinput(yyscanner); #else return input(yyscanner); #endif } case EOB_ACT_CONTINUE_SCAN: yyg->yy_c_buf_p = yyg->yytext_ptr + offset; break; } } } c = *(unsigned char *) yyg->yy_c_buf_p; /* cast for 8-bit char's */ *yyg->yy_c_buf_p = '\0'; /* preserve yytext */ yyg->yy_hold_char = *++yyg->yy_c_buf_p; /* %% [19.0] update BOL and yylineno */ return c; } /* %if-c-only */ #endif /* ifndef YY_NO_INPUT */ /* %endif */ /** Immediately switch to a different input stream. * @param input_file A readable stream. * @param yyscanner The scanner object. * @note This function does not reset the start condition to @c INITIAL . */ /* %if-c-only */ void xhpastrestart (FILE * input_file , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if ( ! YY_CURRENT_BUFFER ){ xhpastensure_buffer_stack (yyscanner); YY_CURRENT_BUFFER_LVALUE = xhpast_create_buffer(yyin,YY_BUF_SIZE ,yyscanner); } xhpast_init_buffer(YY_CURRENT_BUFFER,input_file ,yyscanner); xhpast_load_buffer_state(yyscanner ); } /** Switch to a different input buffer. * @param new_buffer The new input buffer. * @param yyscanner The scanner object. */ /* %if-c-only */ void xhpast_switch_to_buffer (YY_BUFFER_STATE new_buffer , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* TODO. We should be able to replace this entire function body * with * xhpastpop_buffer_state(); * xhpastpush_buffer_state(new_buffer); */ xhpastensure_buffer_stack (yyscanner); if ( YY_CURRENT_BUFFER == new_buffer ) return; if ( YY_CURRENT_BUFFER ) { /* Flush out information for old buffer. */ *yyg->yy_c_buf_p = yyg->yy_hold_char; YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = yyg->yy_c_buf_p; YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars; } YY_CURRENT_BUFFER_LVALUE = new_buffer; xhpast_load_buffer_state(yyscanner ); /* We don't actually know whether we did this switch during * EOF (xhpastwrap()) processing, but the only time this flag * is looked at is after xhpastwrap() is called, so it's safe * to go ahead and always set it. */ yyg->yy_did_buffer_switch_on_eof = 1; } /* %if-c-only */ static void xhpast_load_buffer_state (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yyg->yy_n_chars = YY_CURRENT_BUFFER_LVALUE->yy_n_chars; yyg->yytext_ptr = yyg->yy_c_buf_p = YY_CURRENT_BUFFER_LVALUE->yy_buf_pos; yyin = YY_CURRENT_BUFFER_LVALUE->yy_input_file; yyg->yy_hold_char = *yyg->yy_c_buf_p; } /** Allocate and initialize an input buffer state. * @param file A readable stream. * @param size The character buffer size in bytes. When in doubt, use @c YY_BUF_SIZE. * @param yyscanner The scanner object. * @return the allocated buffer state. */ /* %if-c-only */ YY_BUFFER_STATE xhpast_create_buffer (FILE * file, int size , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { YY_BUFFER_STATE b; b = (YY_BUFFER_STATE) xhpastalloc(sizeof( struct yy_buffer_state ) ,yyscanner ); if ( ! b ) YY_FATAL_ERROR( "out of dynamic memory in xhpast_create_buffer()" ); b->yy_buf_size = size; /* yy_ch_buf has to be 2 characters longer than the size given because * we need to put in 2 end-of-buffer characters. */ b->yy_ch_buf = (char *) xhpastalloc(b->yy_buf_size + 2 ,yyscanner ); if ( ! b->yy_ch_buf ) YY_FATAL_ERROR( "out of dynamic memory in xhpast_create_buffer()" ); b->yy_is_our_buffer = 1; xhpast_init_buffer(b,file ,yyscanner); return b; } /** Destroy the buffer. * @param b a buffer created with xhpast_create_buffer() * @param yyscanner The scanner object. */ /* %if-c-only */ void xhpast_delete_buffer (YY_BUFFER_STATE b , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if ( ! b ) return; if ( b == YY_CURRENT_BUFFER ) /* Not sure if we should pop here. */ YY_CURRENT_BUFFER_LVALUE = (YY_BUFFER_STATE) 0; if ( b->yy_is_our_buffer ) xhpastfree((void *) b->yy_ch_buf ,yyscanner ); xhpastfree((void *) b ,yyscanner ); } /* %if-c-only */ #ifndef __cplusplus extern int isatty (int ); #endif /* __cplusplus */ /* %endif */ /* %if-c++-only */ /* %endif */ /* Initializes or reinitializes a buffer. * This function is sometimes called more than once on the same buffer, * such as during a xhpastrestart() or at EOF. */ /* %if-c-only */ static void xhpast_init_buffer (YY_BUFFER_STATE b, FILE * file , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { int oerrno = errno; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; xhpast_flush_buffer(b ,yyscanner); b->yy_input_file = file; b->yy_fill_buffer = 1; /* If b is the current buffer, then xhpast_init_buffer was _probably_ * called from xhpastrestart() or through yy_get_next_buffer. * In that case, we don't want to reset the lineno or column. */ if (b != YY_CURRENT_BUFFER){ b->yy_bs_lineno = 1; b->yy_bs_column = 0; } /* %if-c-only */ b->yy_is_interactive = file ? (isatty( fileno(file) ) > 0) : 0; /* %endif */ /* %if-c++-only */ /* %endif */ errno = oerrno; } /** Discard all buffered characters. On the next scan, YY_INPUT will be called. * @param b the buffer state to be flushed, usually @c YY_CURRENT_BUFFER. * @param yyscanner The scanner object. */ /* %if-c-only */ void xhpast_flush_buffer (YY_BUFFER_STATE b , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if ( ! b ) return; b->yy_n_chars = 0; /* We always need two end-of-buffer characters. The first causes * a transition to the end-of-buffer state. The second causes * a jam in that state. */ b->yy_ch_buf[0] = YY_END_OF_BUFFER_CHAR; b->yy_ch_buf[1] = YY_END_OF_BUFFER_CHAR; b->yy_buf_pos = &b->yy_ch_buf[0]; b->yy_at_bol = 1; b->yy_buffer_status = YY_BUFFER_NEW; if ( b == YY_CURRENT_BUFFER ) xhpast_load_buffer_state(yyscanner ); } /* %if-c-or-c++ */ /** Pushes the new state onto the stack. The new state becomes * the current state. This function will allocate the stack * if necessary. * @param new_buffer The new state. * @param yyscanner The scanner object. */ /* %if-c-only */ void xhpastpush_buffer_state (YY_BUFFER_STATE new_buffer , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if (new_buffer == NULL) return; xhpastensure_buffer_stack(yyscanner); /* This block is copied from xhpast_switch_to_buffer. */ if ( YY_CURRENT_BUFFER ) { /* Flush out information for old buffer. */ *yyg->yy_c_buf_p = yyg->yy_hold_char; YY_CURRENT_BUFFER_LVALUE->yy_buf_pos = yyg->yy_c_buf_p; YY_CURRENT_BUFFER_LVALUE->yy_n_chars = yyg->yy_n_chars; } /* Only push if top exists. Otherwise, replace top. */ if (YY_CURRENT_BUFFER) yyg->yy_buffer_stack_top++; YY_CURRENT_BUFFER_LVALUE = new_buffer; /* copied from xhpast_switch_to_buffer. */ xhpast_load_buffer_state(yyscanner ); yyg->yy_did_buffer_switch_on_eof = 1; } /* %endif */ /* %if-c-or-c++ */ /** Removes and deletes the top of the stack, if present. * The next element becomes the new top. * @param yyscanner The scanner object. */ /* %if-c-only */ void xhpastpop_buffer_state (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if (!YY_CURRENT_BUFFER) return; xhpast_delete_buffer(YY_CURRENT_BUFFER ,yyscanner); YY_CURRENT_BUFFER_LVALUE = NULL; if (yyg->yy_buffer_stack_top > 0) --yyg->yy_buffer_stack_top; if (YY_CURRENT_BUFFER) { xhpast_load_buffer_state(yyscanner ); yyg->yy_did_buffer_switch_on_eof = 1; } } /* %endif */ /* %if-c-or-c++ */ /* Allocates the stack if it does not exist. * Guarantees space for at least one push. */ /* %if-c-only */ static void xhpastensure_buffer_stack (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { - yy_size_t num_to_alloc; + int num_to_alloc; struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if (!yyg->yy_buffer_stack) { /* First allocation is just for 2 elements, since we don't know if this * scanner will even need a stack. We use 2 instead of 1 to avoid an * immediate realloc on the next call. */ num_to_alloc = 1; yyg->yy_buffer_stack = (struct yy_buffer_state**)xhpastalloc (num_to_alloc * sizeof(struct yy_buffer_state*) , yyscanner); if ( ! yyg->yy_buffer_stack ) YY_FATAL_ERROR( "out of dynamic memory in xhpastensure_buffer_stack()" ); memset(yyg->yy_buffer_stack, 0, num_to_alloc * sizeof(struct yy_buffer_state*)); yyg->yy_buffer_stack_max = num_to_alloc; yyg->yy_buffer_stack_top = 0; return; } if (yyg->yy_buffer_stack_top >= (yyg->yy_buffer_stack_max) - 1){ /* Increase the buffer to prepare for a possible push. */ int grow_size = 8 /* arbitrary grow size */; num_to_alloc = yyg->yy_buffer_stack_max + grow_size; yyg->yy_buffer_stack = (struct yy_buffer_state**)xhpastrealloc (yyg->yy_buffer_stack, num_to_alloc * sizeof(struct yy_buffer_state*) , yyscanner); if ( ! yyg->yy_buffer_stack ) YY_FATAL_ERROR( "out of dynamic memory in xhpastensure_buffer_stack()" ); /* zero only the new slots.*/ memset(yyg->yy_buffer_stack + yyg->yy_buffer_stack_max, 0, grow_size * sizeof(struct yy_buffer_state*)); yyg->yy_buffer_stack_max = num_to_alloc; } } /* %endif */ /* %if-c-only */ /** Setup the input buffer state to scan directly from a user-specified character buffer. * @param base the character buffer * @param size the size in bytes of the character buffer * @param yyscanner The scanner object. * @return the newly allocated buffer state object. */ YY_BUFFER_STATE xhpast_scan_buffer (char * base, yy_size_t size , yyscan_t yyscanner) { YY_BUFFER_STATE b; if ( size < 2 || base[size-2] != YY_END_OF_BUFFER_CHAR || base[size-1] != YY_END_OF_BUFFER_CHAR ) /* They forgot to leave room for the EOB's. */ return 0; b = (YY_BUFFER_STATE) xhpastalloc(sizeof( struct yy_buffer_state ) ,yyscanner ); if ( ! b ) YY_FATAL_ERROR( "out of dynamic memory in xhpast_scan_buffer()" ); b->yy_buf_size = size - 2; /* "- 2" to take care of EOB's */ b->yy_buf_pos = b->yy_ch_buf = base; b->yy_is_our_buffer = 0; b->yy_input_file = 0; b->yy_n_chars = b->yy_buf_size; b->yy_is_interactive = 0; b->yy_at_bol = 1; b->yy_fill_buffer = 0; b->yy_buffer_status = YY_BUFFER_NEW; xhpast_switch_to_buffer(b ,yyscanner ); return b; } /* %endif */ /* %if-c-only */ /** Setup the input buffer state to scan a string. The next call to xhpastlex() will * scan from a @e copy of @a str. * @param yystr a NUL-terminated string to scan * @param yyscanner The scanner object. * @return the newly allocated buffer state object. * @note If you want to scan bytes that may contain NUL values, then use * xhpast_scan_bytes() instead. */ YY_BUFFER_STATE xhpast_scan_string (yyconst char * yystr , yyscan_t yyscanner) { return xhpast_scan_bytes(yystr,strlen(yystr) ,yyscanner); } /* %endif */ /* %if-c-only */ /** Setup the input buffer state to scan the given bytes. The next call to xhpastlex() will * scan from a @e copy of @a bytes. * @param bytes the byte buffer to scan * @param len the number of bytes in the buffer pointed to by @a bytes. * @param yyscanner The scanner object. * @return the newly allocated buffer state object. */ -YY_BUFFER_STATE xhpast_scan_bytes (yyconst char * yybytes, yy_size_t _yybytes_len , yyscan_t yyscanner) +YY_BUFFER_STATE xhpast_scan_bytes (yyconst char * yybytes, int _yybytes_len , yyscan_t yyscanner) { YY_BUFFER_STATE b; char *buf; - yy_size_t n, i; + yy_size_t n; + int i; /* Get memory for full buffer, including space for trailing EOB's. */ n = _yybytes_len + 2; buf = (char *) xhpastalloc(n ,yyscanner ); if ( ! buf ) YY_FATAL_ERROR( "out of dynamic memory in xhpast_scan_bytes()" ); for ( i = 0; i < _yybytes_len; ++i ) buf[i] = yybytes[i]; buf[_yybytes_len] = buf[_yybytes_len+1] = YY_END_OF_BUFFER_CHAR; b = xhpast_scan_buffer(buf,n ,yyscanner); if ( ! b ) YY_FATAL_ERROR( "bad buffer in xhpast_scan_bytes()" ); /* It's okay to grow etc. this buffer, and we should throw it * away when we're done. */ b->yy_is_our_buffer = 1; return b; } /* %endif */ /* %if-c-only */ static void yy_push_state (int new_state , yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if ( yyg->yy_start_stack_ptr >= yyg->yy_start_stack_depth ) { yy_size_t new_size; yyg->yy_start_stack_depth += YY_START_STACK_INCR; new_size = yyg->yy_start_stack_depth * sizeof( int ); if ( ! yyg->yy_start_stack ) yyg->yy_start_stack = (int *) xhpastalloc(new_size ,yyscanner ); else yyg->yy_start_stack = (int *) xhpastrealloc((void *) yyg->yy_start_stack,new_size ,yyscanner ); if ( ! yyg->yy_start_stack ) YY_FATAL_ERROR( "out of memory expanding start-condition stack" ); } yyg->yy_start_stack[yyg->yy_start_stack_ptr++] = YY_START; BEGIN(new_state); } /* %if-c-only */ static void yy_pop_state (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if ( --yyg->yy_start_stack_ptr < 0 ) YY_FATAL_ERROR( "start-condition stack underflow" ); BEGIN(yyg->yy_start_stack[yyg->yy_start_stack_ptr]); } /* %if-c-only */ static int yy_top_state (yyscan_t yyscanner) /* %endif */ /* %if-c++-only */ /* %endif */ { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyg->yy_start_stack[yyg->yy_start_stack_ptr - 1]; } #ifndef YY_EXIT_FAILURE #define YY_EXIT_FAILURE 2 #endif /* %if-c-only */ static void yy_fatal_error (yyconst char* msg , yyscan_t yyscanner) { (void) fprintf( stderr, "%s\n", msg ); exit( YY_EXIT_FAILURE ); } /* %endif */ /* %if-c++-only */ /* %endif */ /* Redefine yyless() so it works in section 3 code. */ #undef yyless #define yyless(n) \ do \ { \ /* Undo effects of setting up yytext. */ \ int yyless_macro_arg = (n); \ YY_LESS_LINENO(yyless_macro_arg);\ yytext[yyleng] = yyg->yy_hold_char; \ yyg->yy_c_buf_p = yytext + yyless_macro_arg; \ yyg->yy_hold_char = *yyg->yy_c_buf_p; \ *yyg->yy_c_buf_p = '\0'; \ yyleng = yyless_macro_arg; \ } \ while ( 0 ) /* Accessor methods (get/set functions) to struct members. */ /* %if-c-only */ /* %if-reentrant */ /** Get the user-defined data for this scanner. * @param yyscanner The scanner object. */ YY_EXTRA_TYPE xhpastget_extra (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyextra; } /* %endif */ /** Get the current line number. * @param yyscanner The scanner object. */ int xhpastget_lineno (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if (! YY_CURRENT_BUFFER) return 0; return yylineno; } /** Get the current column number. * @param yyscanner The scanner object. */ int xhpastget_column (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; if (! YY_CURRENT_BUFFER) return 0; return yycolumn; } /** Get the input stream. * @param yyscanner The scanner object. */ FILE *xhpastget_in (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyin; } /** Get the output stream. * @param yyscanner The scanner object. */ FILE *xhpastget_out (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyout; } /** Get the length of the current token. * @param yyscanner The scanner object. */ -yy_size_t xhpastget_leng (yyscan_t yyscanner) +int xhpastget_leng (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yyleng; } /** Get the current token. * @param yyscanner The scanner object. */ char *xhpastget_text (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yytext; } /* %if-reentrant */ /** Set the user-defined data. This data is never touched by the scanner. * @param user_defined The data to be associated with this scanner. * @param yyscanner The scanner object. */ void xhpastset_extra (YY_EXTRA_TYPE user_defined , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yyextra = user_defined ; } /* %endif */ /** Set the current line number. * @param line_number * @param yyscanner The scanner object. */ void xhpastset_lineno (int line_number , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* lineno is only valid if an input buffer exists. */ if (! YY_CURRENT_BUFFER ) yy_fatal_error( "xhpastset_lineno called with no buffer" , yyscanner); yylineno = line_number; } /** Set the current column. * @param line_number * @param yyscanner The scanner object. */ void xhpastset_column (int column_no , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* column is only valid if an input buffer exists. */ if (! YY_CURRENT_BUFFER ) yy_fatal_error( "xhpastset_column called with no buffer" , yyscanner); yycolumn = column_no; } /** Set the input stream. This does not discard the current * input buffer. * @param in_str A readable stream. * @param yyscanner The scanner object. * @see xhpast_switch_to_buffer */ void xhpastset_in (FILE * in_str , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yyin = in_str ; } void xhpastset_out (FILE * out_str , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yyout = out_str ; } int xhpastget_debug (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yy_flex_debug; } void xhpastset_debug (int bdebug , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yy_flex_debug = bdebug ; } /* %endif */ /* %if-reentrant */ /* Accessor methods for yylval and yylloc */ /* %if-bison-bridge */ YYSTYPE * xhpastget_lval (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; return yylval; } void xhpastset_lval (YYSTYPE * yylval_param , yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; yylval = yylval_param; } /* %endif */ /* User-visible API */ /* xhpastlex_init is special because it creates the scanner itself, so it is * the ONLY reentrant function that doesn't take the scanner as the last argument. * That's why we explicitly handle the declaration, instead of using our macros. */ int xhpastlex_init(yyscan_t* ptr_yy_globals) { if (ptr_yy_globals == NULL){ errno = EINVAL; return 1; } *ptr_yy_globals = (yyscan_t) xhpastalloc ( sizeof( struct yyguts_t ), NULL ); if (*ptr_yy_globals == NULL){ errno = ENOMEM; return 1; } /* By setting to 0xAA, we expose bugs in yy_init_globals. Leave at 0x00 for releases. */ memset(*ptr_yy_globals,0x00,sizeof(struct yyguts_t)); return yy_init_globals ( *ptr_yy_globals ); } /* xhpastlex_init_extra has the same functionality as xhpastlex_init, but follows the * convention of taking the scanner as the last argument. Note however, that * this is a *pointer* to a scanner, as it will be allocated by this call (and * is the reason, too, why this function also must handle its own declaration). * The user defined value in the first argument will be available to xhpastalloc in * the yyextra field. */ int xhpastlex_init_extra(YY_EXTRA_TYPE yy_user_defined,yyscan_t* ptr_yy_globals ) { struct yyguts_t dummy_yyguts; xhpastset_extra (yy_user_defined, &dummy_yyguts); if (ptr_yy_globals == NULL){ errno = EINVAL; return 1; } *ptr_yy_globals = (yyscan_t) xhpastalloc ( sizeof( struct yyguts_t ), &dummy_yyguts ); if (*ptr_yy_globals == NULL){ errno = ENOMEM; return 1; } /* By setting to 0xAA, we expose bugs in yy_init_globals. Leave at 0x00 for releases. */ memset(*ptr_yy_globals,0x00,sizeof(struct yyguts_t)); xhpastset_extra (yy_user_defined, *ptr_yy_globals); return yy_init_globals ( *ptr_yy_globals ); } /* %endif if-c-only */ /* %if-c-only */ static int yy_init_globals (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* Initialization is the same as for the non-reentrant scanner. * This function is called from xhpastlex_destroy(), so don't allocate here. */ yyg->yy_buffer_stack = 0; yyg->yy_buffer_stack_top = 0; yyg->yy_buffer_stack_max = 0; yyg->yy_c_buf_p = (char *) 0; yyg->yy_init = 0; yyg->yy_start = 0; yyg->yy_start_stack_ptr = 0; yyg->yy_start_stack_depth = 0; yyg->yy_start_stack = NULL; /* Defined in main.c */ #ifdef YY_STDINIT yyin = stdin; yyout = stdout; #else yyin = (FILE *) 0; yyout = (FILE *) 0; #endif /* For future reference: Set errno on error, since we are called by * xhpastlex_init() */ return 0; } /* %endif */ /* %if-c-only SNIP! this currently causes conflicts with the c++ scanner */ /* xhpastlex_destroy is for both reentrant and non-reentrant scanners. */ int xhpastlex_destroy (yyscan_t yyscanner) { struct yyguts_t * yyg = (struct yyguts_t*)yyscanner; /* Pop the buffer stack, destroying each element. */ while(YY_CURRENT_BUFFER){ xhpast_delete_buffer(YY_CURRENT_BUFFER ,yyscanner ); YY_CURRENT_BUFFER_LVALUE = NULL; xhpastpop_buffer_state(yyscanner); } /* Destroy the stack itself. */ xhpastfree(yyg->yy_buffer_stack ,yyscanner); yyg->yy_buffer_stack = NULL; /* Destroy the start condition stack. */ xhpastfree(yyg->yy_start_stack ,yyscanner ); yyg->yy_start_stack = NULL; /* Reset the globals. This is important in a non-reentrant scanner so the next time * xhpastlex() is called, initialization will occur. */ yy_init_globals( yyscanner); /* %if-reentrant */ /* Destroy the main struct (reentrant only). */ xhpastfree ( yyscanner , yyscanner ); yyscanner = NULL; /* %endif */ return 0; } /* %endif */ /* * Internal utility routines. */ #ifndef yytext_ptr static void yy_flex_strncpy (char* s1, yyconst char * s2, int n , yyscan_t yyscanner) { register int i; for ( i = 0; i < n; ++i ) s1[i] = s2[i]; } #endif #ifdef YY_NEED_STRLEN static int yy_flex_strlen (yyconst char * s , yyscan_t yyscanner) { register int n; for ( n = 0; s[n]; ++n ) ; return n; } #endif void *xhpastalloc (yy_size_t size , yyscan_t yyscanner) { return (void *) malloc( size ); } void *xhpastrealloc (void * ptr, yy_size_t size , yyscan_t yyscanner) { /* The cast to (char *) in the following accommodates both * implementations that use char* generic pointers, and those * that use void* generic pointers. It works with the latter * because both ANSI C and C++ allow castless assignment from * any pointer type to void*, and deal with argument conversions * as though doing an assignment. */ return (void *) realloc( (char *) ptr, size ); } void xhpastfree (void * ptr , yyscan_t yyscanner) { free( (char *) ptr ); /* see xhpastrealloc() for (char *) cast */ } /* %if-tables-serialization definitions */ /* %define-yytables The name for this specific scanner's tables. */ #define YYTABLES_NAME "yytables" /* %endif */ /* %ok-for-header */ -#line 414 "scanner.l" +#line 398 "scanner.l" #ifdef DEBUG static const char* yy_state_name(int state) { switch (state) { case INITIAL: return "INITIAL"; case PHP: return "PHP"; case PHP_COMMENT: return "PHP_COMMENT"; case PHP_EOL_COMMENT: return "PHP_EOL_COMMENT"; case PHP_DOC_COMMENT: return "PHP_DOC_COMMENT"; case PHP_HEREDOC_START: return "PHP_HEREDOC_START"; case PHP_HEREDOC_NSTART: return "PHP_HEREDOC_NSTART"; case PHP_HEREDOC_NEWLINE: return "PHP_HEREDOC_NEWLINE"; case PHP_HEREDOC_DATA: return "PHP_HEREDOC_DATA"; case PHP_NO_RESERVED_WORDS: return "PHP_NO_RESERVED_WORDS"; case PHP_NO_RESERVED_WORDS_PERSIST: return "PHP_NO_RESERVED_WORDS_PERSIST"; default: return "???"; } } static void yy_log_token(int tok) { const char* tokname = yytokname(tok); if (tokname) { fprintf(stderr, "--> %s\n", tokname); } else { fprintf(stderr, "--> '%c'\n", tok); } } #endif static int yy_token(int tok, yyguts_t* yyg) { if (YY_START == PHP_NO_RESERVED_WORDS) { pop_state(); } switch (tok) { case T_OPEN_TAG: case T_OPEN_TAG_WITH_ECHO: case T_OPEN_TAG_FAKE: push_state(PHP); break; case T_CLOSE_TAG: pop_state(); // We need to return a ';', not a T_CLOSE_TAG, because a construct like // "" is valid and there are about a billion parser rules // which terminate with ';' so making a new rule like // "semicolon_or_close_tag" would be hard. The token in yylval has the // correct type and value, we just don't generate a node. return ';'; // In PHP it's ok to use keywords such as 'if' as field names // or function names. case T_OBJECT_OPERATOR: case T_FUNCTION: push_state(PHP_NO_RESERVED_WORDS); break; case T_PAAMAYIM_NEKUDOTAYIM: if (yyextra->colon_hack) { yyextra->colon_hack = false; } else { push_state(PHP_NO_RESERVED_WORDS); } break; case '{': // not used anymore yyextra->curly_stack.push(tok); break; } #ifdef DEBUG yy_log_token(tok); #endif return yyextra->last_token = tok; } static inline void yy_scan_newlines(const char* text, struct yyguts_t* yyg) { for (; *text; ++text) { if (*text == '\r') { if (text[1] == '\n') { ++text; } ++yyextra->lineno; } else if (*text == '\n') { ++yyextra->lineno; } } } void xhp_new_push_state(int s, struct yyguts_t* yyg) { #ifdef DEBUG fprintf(stderr, "--> PUSH(%s -> %s)\n", yy_state_name(YY_START), yy_state_name(s)); #endif yy_push_state(s, yyg); } void xhp_new_pop_state(struct yyguts_t* yyg) { #ifdef DEBUG int s = YY_START; #endif yy_pop_state(yyg); #ifdef DEBUG fprintf(stderr, "--> POP(%s -> %s)\n", yy_state_name(s), yy_state_name(YY_START)); #endif } void xhp_set_state(int s, struct yyguts_t* yyg) { #ifdef DEBUG fprintf(stderr, "--> SET(%s)\n", yy_state_name(s)); #endif BEGIN(s); } /* @generated */ diff --git a/support/xhpast/scanner.lex.hpp b/support/xhpast/scanner.lex.hpp index ff4add9..85badfa 100644 --- a/support/xhpast/scanner.lex.hpp +++ b/support/xhpast/scanner.lex.hpp @@ -1,472 +1,472 @@ #ifndef xhpastHEADER_H #define xhpastHEADER_H 1 #define xhpastIN_HEADER 1 #line 6 "scanner.lex.hpp" #line 8 "scanner.lex.hpp" #define YY_INT_ALIGNED short int /* A lexical scanner generated by flex */ /* %not-for-header */ #define FLEX_SCANNER #define YY_FLEX_MAJOR_VERSION 2 #define YY_FLEX_MINOR_VERSION 5 #define YY_FLEX_SUBMINOR_VERSION 35 #if YY_FLEX_SUBMINOR_VERSION > 0 #define FLEX_BETA #endif /* %if-c++-only */ /* %endif */ /* %if-c-only */ /* %endif */ /* %if-c-only */ /* %endif */ /* First, we deal with platform-specific or compiler-specific issues. */ /* begin standard C headers. */ /* %if-c-only */ #include #include #include #include /* %endif */ /* %if-tables-serialization */ /* %endif */ /* end standard C headers. */ /* %if-c-or-c++ */ /* flex integer type definitions */ #ifndef FLEXINT_H #define FLEXINT_H /* C99 systems have . Non-C99 systems may or may not. */ #if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 says to define __STDC_LIMIT_MACROS before including stdint.h, * if you want the limit (max/min) macros for int types. */ #ifndef __STDC_LIMIT_MACROS #define __STDC_LIMIT_MACROS 1 #endif #include typedef int8_t flex_int8_t; typedef uint8_t flex_uint8_t; typedef int16_t flex_int16_t; typedef uint16_t flex_uint16_t; typedef int32_t flex_int32_t; typedef uint32_t flex_uint32_t; #else typedef signed char flex_int8_t; typedef short int flex_int16_t; typedef int flex_int32_t; typedef unsigned char flex_uint8_t; typedef unsigned short int flex_uint16_t; typedef unsigned int flex_uint32_t; #endif /* ! C99 */ /* Limits of integral types. */ #ifndef INT8_MIN #define INT8_MIN (-128) #endif #ifndef INT16_MIN #define INT16_MIN (-32767-1) #endif #ifndef INT32_MIN #define INT32_MIN (-2147483647-1) #endif #ifndef INT8_MAX #define INT8_MAX (127) #endif #ifndef INT16_MAX #define INT16_MAX (32767) #endif #ifndef INT32_MAX #define INT32_MAX (2147483647) #endif #ifndef UINT8_MAX #define UINT8_MAX (255U) #endif #ifndef UINT16_MAX #define UINT16_MAX (65535U) #endif #ifndef UINT32_MAX #define UINT32_MAX (4294967295U) #endif #endif /* ! FLEXINT_H */ /* %endif */ /* %if-c++-only */ /* %endif */ #ifdef __cplusplus /* The "const" storage-class-modifier is valid. */ #define YY_USE_CONST #else /* ! __cplusplus */ /* C99 requires __STDC__ to be defined as 1. */ #if defined (__STDC__) #define YY_USE_CONST #endif /* defined (__STDC__) */ #endif /* ! __cplusplus */ #ifdef YY_USE_CONST #define yyconst const #else #define yyconst #endif /* %not-for-header */ /* %not-for-header */ /* %if-reentrant */ /* An opaque pointer. */ #ifndef YY_TYPEDEF_YY_SCANNER_T #define YY_TYPEDEF_YY_SCANNER_T typedef void* yyscan_t; #endif /* For convenience, these vars (plus the bison vars far below) are macros in the reentrant scanner. */ #define yyin yyg->yyin_r #define yyout yyg->yyout_r #define yyextra yyg->yyextra_r #define yyleng yyg->yyleng_r #define yytext yyg->yytext_r #define yylineno (YY_CURRENT_BUFFER_LVALUE->yy_bs_lineno) #define yycolumn (YY_CURRENT_BUFFER_LVALUE->yy_bs_column) #define yy_flex_debug yyg->yy_flex_debug_r /* %endif */ /* %if-not-reentrant */ /* %endif */ /* Size of default input buffer. */ #ifndef YY_BUF_SIZE #define YY_BUF_SIZE 16384 #endif #ifndef YY_TYPEDEF_YY_BUFFER_STATE #define YY_TYPEDEF_YY_BUFFER_STATE typedef struct yy_buffer_state *YY_BUFFER_STATE; #endif -#ifndef YY_TYPEDEF_YY_SIZE_T -#define YY_TYPEDEF_YY_SIZE_T -typedef size_t yy_size_t; -#endif - /* %if-not-reentrant */ /* %endif */ /* %if-c-only */ /* %if-not-reentrant */ /* %endif */ /* %endif */ +#ifndef YY_TYPEDEF_YY_SIZE_T +#define YY_TYPEDEF_YY_SIZE_T +typedef size_t yy_size_t; +#endif + #ifndef YY_STRUCT_YY_BUFFER_STATE #define YY_STRUCT_YY_BUFFER_STATE struct yy_buffer_state { /* %if-c-only */ FILE *yy_input_file; /* %endif */ /* %if-c++-only */ /* %endif */ char *yy_ch_buf; /* input buffer */ char *yy_buf_pos; /* current position in input buffer */ /* Size of input buffer in bytes, not including room for EOB * characters. */ yy_size_t yy_buf_size; /* Number of characters read into yy_ch_buf, not including EOB * characters. */ - yy_size_t yy_n_chars; + int yy_n_chars; /* Whether we "own" the buffer - i.e., we know we created it, * and can realloc() it to grow it, and should free() it to * delete it. */ int yy_is_our_buffer; /* Whether this is an "interactive" input source; if so, and * if we're using stdio for input, then we want to use getc() * instead of fread(), to make sure we stop fetching input after * each newline. */ int yy_is_interactive; /* Whether we're considered to be at the beginning of a line. * If so, '^' rules will be active on the next match, otherwise * not. */ int yy_at_bol; int yy_bs_lineno; /**< The line count. */ int yy_bs_column; /**< The column count. */ /* Whether to try to fill the input buffer when we reach the * end of it. */ int yy_fill_buffer; int yy_buffer_status; }; #endif /* !YY_STRUCT_YY_BUFFER_STATE */ /* %if-c-only Standard (non-C++) definition */ /* %not-for-header */ /* %endif */ /* %if-c-only Standard (non-C++) definition */ /* %if-not-reentrant */ /* %not-for-header */ /* %endif */ void xhpastrestart (FILE *input_file ,yyscan_t yyscanner ); void xhpast_switch_to_buffer (YY_BUFFER_STATE new_buffer ,yyscan_t yyscanner ); YY_BUFFER_STATE xhpast_create_buffer (FILE *file,int size ,yyscan_t yyscanner ); void xhpast_delete_buffer (YY_BUFFER_STATE b ,yyscan_t yyscanner ); void xhpast_flush_buffer (YY_BUFFER_STATE b ,yyscan_t yyscanner ); void xhpastpush_buffer_state (YY_BUFFER_STATE new_buffer ,yyscan_t yyscanner ); void xhpastpop_buffer_state (yyscan_t yyscanner ); YY_BUFFER_STATE xhpast_scan_buffer (char *base,yy_size_t size ,yyscan_t yyscanner ); YY_BUFFER_STATE xhpast_scan_string (yyconst char *yy_str ,yyscan_t yyscanner ); -YY_BUFFER_STATE xhpast_scan_bytes (yyconst char *bytes,yy_size_t len ,yyscan_t yyscanner ); +YY_BUFFER_STATE xhpast_scan_bytes (yyconst char *bytes,int len ,yyscan_t yyscanner ); /* %endif */ void *xhpastalloc (yy_size_t ,yyscan_t yyscanner ); void *xhpastrealloc (void *,yy_size_t ,yyscan_t yyscanner ); void xhpastfree (void * ,yyscan_t yyscanner ); /* %% [1.0] yytext/yyin/yyout/yy_state_type/yylineno etc. def's & init go here */ /* Begin user sect3 */ #define xhpastwrap(n) 1 #define YY_SKIP_YYWRAP #define FLEX_DEBUG #define yytext_ptr yytext_r /* %if-c-only Standard (non-C++) definition */ /* %endif */ #ifdef YY_HEADER_EXPORT_START_CONDITIONS #define INITIAL 0 #define PHP 1 #define PHP_COMMENT 2 #define PHP_EOL_COMMENT 3 #define PHP_DOC_COMMENT 4 #define PHP_HEREDOC_START 5 #define PHP_HEREDOC_NSTART 6 #define PHP_HEREDOC_NEWLINE 7 #define PHP_HEREDOC_DATA 8 #define PHP_NO_RESERVED_WORDS 9 #define PHP_NO_RESERVED_WORDS_PERSIST 10 #define PHP_ 11 #endif #ifndef YY_NO_UNISTD_H /* Special case for "unistd.h", since it is non-ANSI. We include it way * down here because we want the user's section 1 to have been scanned first. * The user has a chance to override it with an option. */ /* %if-c-only */ #include /* %endif */ /* %if-c++-only */ /* %endif */ #endif #ifndef YY_EXTRA_TYPE #define YY_EXTRA_TYPE void * #endif /* %if-c-only Reentrant structure and macros (non-C++). */ /* %if-reentrant */ /* %if-c-only */ /* %endif */ /* %if-reentrant */ int xhpastlex_init (yyscan_t* scanner); int xhpastlex_init_extra (YY_EXTRA_TYPE user_defined,yyscan_t* scanner); /* %endif */ /* %endif End reentrant structures and macros. */ /* Accessor methods to globals. These are made visible to non-reentrant scanners for convenience. */ int xhpastlex_destroy (yyscan_t yyscanner ); int xhpastget_debug (yyscan_t yyscanner ); void xhpastset_debug (int debug_flag ,yyscan_t yyscanner ); YY_EXTRA_TYPE xhpastget_extra (yyscan_t yyscanner ); void xhpastset_extra (YY_EXTRA_TYPE user_defined ,yyscan_t yyscanner ); FILE *xhpastget_in (yyscan_t yyscanner ); void xhpastset_in (FILE * in_str ,yyscan_t yyscanner ); FILE *xhpastget_out (yyscan_t yyscanner ); void xhpastset_out (FILE * out_str ,yyscan_t yyscanner ); -yy_size_t xhpastget_leng (yyscan_t yyscanner ); +int xhpastget_leng (yyscan_t yyscanner ); char *xhpastget_text (yyscan_t yyscanner ); int xhpastget_lineno (yyscan_t yyscanner ); void xhpastset_lineno (int line_number ,yyscan_t yyscanner ); /* %if-bison-bridge */ YYSTYPE * xhpastget_lval (yyscan_t yyscanner ); void xhpastset_lval (YYSTYPE * yylval_param ,yyscan_t yyscanner ); /* %endif */ /* Macros after this point can all be overridden by user definitions in * section 1. */ #ifndef YY_SKIP_YYWRAP #ifdef __cplusplus extern "C" int xhpastwrap (yyscan_t yyscanner ); #else extern int xhpastwrap (yyscan_t yyscanner ); #endif #endif /* %not-for-header */ /* %endif */ #ifndef yytext_ptr static void yy_flex_strncpy (char *,yyconst char *,int ,yyscan_t yyscanner); #endif #ifdef YY_NEED_STRLEN static int yy_flex_strlen (yyconst char * ,yyscan_t yyscanner); #endif #ifndef YY_NO_INPUT /* %if-c-only Standard (non-C++) definition */ /* %not-for-header */ /* %endif */ #endif /* %if-c-only */ /* %endif */ /* Amount of stuff to slurp up with each read. */ #ifndef YY_READ_BUF_SIZE #define YY_READ_BUF_SIZE 8192 #endif /* Number of entries by which start-condition stack grows. */ #ifndef YY_START_STACK_INCR #define YY_START_STACK_INCR 25 #endif /* %if-tables-serialization structures and prototypes */ /* %not-for-header */ /* %not-for-header */ /* Default declaration of generated scanner - a define so the user can * easily add parameters. */ #ifndef YY_DECL #define YY_DECL_IS_OURS 1 /* %if-c-only Standard (non-C++) definition */ extern int xhpastlex \ (YYSTYPE * yylval_param ,yyscan_t yyscanner); #define YY_DECL int xhpastlex \ (YYSTYPE * yylval_param , yyscan_t yyscanner) /* %endif */ /* %if-c++-only C++ definition */ /* %endif */ #endif /* !YY_DECL */ /* %not-for-header */ /* %if-c++-only */ /* %not-for-header */ /* %endif */ /* yy_get_previous_state - get the state just before the EOB char was reached */ /* %if-c-only */ /* %not-for-header */ #undef YY_NEW_FILE #undef YY_FLUSH_BUFFER #undef yy_set_bol #undef yy_new_buffer #undef yy_set_interactive #undef YY_DO_BEFORE_ACTION #ifdef YY_DECL_IS_OURS #undef YY_DECL_IS_OURS #undef YY_DECL #endif -#line 414 "scanner.l" +#line 398 "scanner.l" #line 470 "scanner.lex.hpp" #undef xhpastIN_HEADER #endif /* xhpastHEADER_H */ /* @generated */ diff --git a/support/xhpast/xhpast.cpp b/support/xhpast/xhpast.cpp index cb7688f..26084b0 100644 --- a/support/xhpast/xhpast.cpp +++ b/support/xhpast/xhpast.cpp @@ -1,147 +1,131 @@ -/* - * Copyright 2012 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. - */ - #include "ast.hpp" #include #include #include #include #include using namespace std; int xhpastparse(void*, xhpast::Node **); int xhpast_process(std::string &in); void print_node(xhpast::Node *node); int main(int argc, char* argv[]) { vector files; if (argc != 1) { //coupling: modify also libphutil/src/parser/xhpast/bin/xhpast_parse.php cout << "xhpast version 0.62\n"; return 0; } ifstream inputFile; istream *inputStream; // inputFile.open(argv[1]); // inputStream = &inputFile; inputStream = &cin; std::stringbuf sb; *inputStream >> noskipws >> &sb; std::string buffer = sb.str(); inputFile.close(); return xhpast_process(buffer); } int xhpast_process(std::string &in) { char *buffer; in.reserve(in.size() + 1); buffer = const_cast(in.c_str()); buffer[in.size() + 1] = 0; // need double NULL for scan_buffer void* scanner; yy_extra_type extra; extra.idx_expr = true;//flags.idx_expr; extra.include_debug = true;//flags.include_debug; extra.insert_token = 0;//flags.eval ? T_OPEN_TAG_FAKE : 0; extra.short_tags = true;//flags.short_tags; extra.asp_tags = false;//flags.asp_tags; xhpast::Node *root = NULL; xhpastlex_init(&scanner); xhpastset_extra(&extra, scanner); xhpast_scan_buffer(buffer, in.size() + 2, scanner); xhpastparse(scanner, &root); xhpastlex_destroy(scanner); if (extra.terminated) { fprintf( stderr, "XHPAST Parse Error: %s on line %d\n", extra.error.c_str(), (int)extra.lineno); return 1; } printf("{"); printf("\"tree\":"); if (root) { // Extend the right token for the root node to the end of the concrete // token stream. This ensure all tokens appear in the tree. If we don't // do this and the file ends in tokens which don't go to the parser (like // comments and whitespace) they won't be represented in the tree. root->r_tok = (extra.token_list.size() - 1); print_node(root); } else { printf("null"); } printf(","); printf("\"stream\":"); printf("["); if (!extra.token_list.empty()) { for (xhpast::token_list_t::iterator ii = extra.token_list.begin();;) { printf("[%d, %d]", (*ii)->type, (int)(*ii)->value.length()); if (++ii != extra.token_list.end()) { printf(","); } else { break; } } } printf("]"); printf("}\n"); return 0; } void print_node(xhpast::Node *node) { int l = -1; int r = -1; if (node->l_tok != -1) { l = node->l_tok; } if (l == -1) { printf("[%d]", node->type); } else { if (node->r_tok != -1) { r = node->r_tok; } printf("[%d, %d, %d", node->type, l, r); if (!node->children.empty()) { printf(", ["); for (xhpast::node_list_t::iterator ii = node->children.begin();;) { print_node(*ii); if (++ii != node->children.end()) { printf(","); } else { break; } } printf("]"); } printf("]"); } }