1<?php declare(strict_types=1); 2 3/** @var PhpFuzzer\Fuzzer $fuzzer */ 4 5use PhpParser\Node\Expr; 6use PhpParser\Node\Scalar; 7use PhpParser\Node\Stmt; 8use PhpParser\NodeVisitor; 9 10if (class_exists(PhpParser\Parser\Php7::class)) { 11 echo "The PHP-Parser target can only be used with php-fuzzer.phar,\n"; 12 echo "otherwise there is a conflict with php-fuzzer's own use of PHP-Parser.\n"; 13 exit(1); 14} 15 16$autoload = __DIR__ . '/../../vendor/autoload.php'; 17if (!file_exists($autoload)) { 18 echo "Cannot find PHP-Parser installation in " . __DIR__ . "/PHP-Parser\n"; 19 exit(1); 20} 21 22require $autoload; 23 24$lexer = new PhpParser\Lexer(); 25$parser = new PhpParser\Parser\Php7($lexer); 26$prettyPrinter = new PhpParser\PrettyPrinter\Standard(); 27$nodeDumper = new PhpParser\NodeDumper(); 28$visitor = new class extends PhpParser\NodeVisitorAbstract { 29 private const CAST_NAMES = [ 30 'int', 'integer', 31 'double', 'float', 'real', 32 'string', 'binary', 33 'array', 'object', 34 'bool', 'boolean', 35 'unset', 36 ]; 37 38 39 private $tokens; 40 public $hasProblematicConstruct; 41 42 public function setTokens(array $tokens): void { 43 $this->tokens = $tokens; 44 } 45 46 public function beforeTraverse(array $nodes): void { 47 $this->hasProblematicConstruct = false; 48 } 49 50 public function leaveNode(PhpParser\Node $node) { 51 // We don't precisely preserve nop statements. 52 if ($node instanceof Stmt\Nop) { 53 return NodeVisitor::REMOVE_NODE; 54 } 55 56 // We don't precisely preserve redundant trailing commas in array destructuring. 57 if ($node instanceof Expr\List_) { 58 while (!empty($node->items) && $node->items[count($node->items) - 1] === null) { 59 array_pop($node->items); 60 } 61 } 62 63 // For T_NUM_STRING the parser produced negative integer literals. Convert these into 64 // a unary minus followed by a positive integer. 65 if ($node instanceof Scalar\Int_ && $node->value < 0) { 66 if ($node->value === \PHP_INT_MIN) { 67 // PHP_INT_MIN == -PHP_INT_MAX - 1 68 return new Expr\BinaryOp\Minus( 69 new Expr\UnaryMinus(new Scalar\Int_(\PHP_INT_MAX)), 70 new Scalar\Int_(1)); 71 } 72 return new Expr\UnaryMinus(new Scalar\Int_(-$node->value)); 73 } 74 75 // If a constant with the same name as a cast operand occurs inside parentheses, it will 76 // be parsed back as a cast. E.g. "foo(int)" will fail to parse, because the argument is 77 // interpreted as a cast. We can run into this with inputs like "foo(int\n)", where the 78 // newline is not preserved. 79 if ($node instanceof Expr\ConstFetch && $node->name->isUnqualified() && 80 in_array($node->name->toLowerString(), self::CAST_NAMES) 81 ) { 82 $this->hasProblematicConstruct = true; 83 } 84 85 // The parser does not distinguish between use X and use \X, as they are semantically 86 // equivalent. However, use \keyword is legal PHP, while use keyword is not, so we inspect 87 // tokens to detect this situation here. 88 if ($node instanceof Stmt\Use_ && $node->uses[0]->name->isUnqualified() && 89 $this->tokens[$node->uses[0]->name->getStartTokenPos()]->is(\T_NAME_FULLY_QUALIFIED) 90 ) { 91 $this->hasProblematicConstruct = true; 92 } 93 if ($node instanceof Stmt\GroupUse && $node->prefix->isUnqualified() && 94 $this->tokens[$node->prefix->getStartTokenPos()]->is(\T_NAME_FULLY_QUALIFIED) 95 ) { 96 $this->hasProblematicConstruct = true; 97 } 98 } 99}; 100$traverser = new PhpParser\NodeTraverser(); 101$traverser->addVisitor($visitor); 102 103$fuzzer->setTarget(function(string $input) use($lexer, $parser, $prettyPrinter, $nodeDumper, $visitor, $traverser) { 104 $stmts = $parser->parse($input); 105 $printed = $prettyPrinter->prettyPrintFile($stmts); 106 107 $visitor->setTokens($parser->getTokens()); 108 $stmts = $traverser->traverse($stmts); 109 if ($visitor->hasProblematicConstruct) { 110 return; 111 } 112 113 try { 114 $printedStmts = $parser->parse($printed); 115 } catch (PhpParser\Error $e) { 116 throw new Error("Failed to parse pretty printer output"); 117 } 118 119 $visitor->setTokens($parser->getTokens()); 120 $printedStmts = $traverser->traverse($printedStmts); 121 $same = $nodeDumper->dump($stmts) == $nodeDumper->dump($printedStmts); 122 if (!$same && !preg_match('/<\?php<\?php/i', $input)) { 123 throw new Error("Result after pretty printing differs"); 124 } 125}); 126 127$fuzzer->setMaxLen(1024); 128$fuzzer->addDictionary(__DIR__ . '/php.dict'); 129$fuzzer->setAllowedExceptions([PhpParser\Error::class]); 130