Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Zend/tests/phpc/001_basic.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
.phpc: a file without <?php is parsed as pure PHP
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, 'echo "hello\n"; $x = 1 + 2; echo $x, "\n";');
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
hello
3
12 changes: 12 additions & 0 deletions Zend/tests/phpc/002_php_unchanged.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
.phpc: classic .php behavior is completely unaffected (BC sanity check)
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '_payload.php';
/* Same payload as .phpc test, but written to a .php file. */
file_put_contents($file, 'echo "hello\n"; $x = 1 + 2; echo $x, "\n";');
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
echo "hello\n"; $x = 1 + 2; echo $x, "\n";
15 changes: 15 additions & 0 deletions Zend/tests/phpc/003_phpc_requires_php.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
--TEST--
.phpc: a pure-PHP file can require a classic .php file
--FILE--
<?php
$dir = __DIR__;
$base = basename(__FILE__, '.php');
$phpc = "$dir/{$base}_main.phpc";
$php = "$dir/{$base}_lib.php";
file_put_contents($php, '<?php function greet(string $w): string { return "hi, $w"; }');
file_put_contents($phpc, "require '$php';\necho greet('world'), \"\\n\";");
register_shutdown_function(function () use ($phpc, $php) { @unlink($phpc); @unlink($php); });
require $phpc;
?>
--EXPECT--
hi, world
14 changes: 14 additions & 0 deletions Zend/tests/phpc/004_php_requires_phpc.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
.phpc: a classic .php file can require a .phpc file
--FILE--
<?php
$dir = __DIR__;
$base = basename(__FILE__, '.php');
$phpc = "$dir/{$base}_lib.phpc";
file_put_contents($phpc, "function add(int \$a, int \$b): int { return \$a + \$b; }");
register_shutdown_function(fn() => @unlink($phpc));
require $phpc;
echo add(2, 3), "\n";
?>
--EXPECT--
5
11 changes: 11 additions & 0 deletions Zend/tests/phpc/005_utf8_bom.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
.phpc: a leading UTF-8 BOM is silently skipped
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, "\xEF\xBB\xBF" . 'echo "bom-ok\n";');
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
bom-ok
25 changes: 25 additions & 0 deletions Zend/tests/phpc/006_halt_compiler.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
.phpc: __halt_compiler() works and __COMPILER_HALT_OFFSET__ points to trailing data
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
/* The .phpc file echoes a marker, halts the compiler, and the driver here
* reads what comes after __halt_compiler() back via the offset constant
* (which __halt_compiler() exposes in the loaded file's namespace). */
$code = <<<'PHPC'
$offset = __COMPILER_HALT_OFFSET__;
$fh = fopen(__FILE__, 'rb');
fseek($fh, $offset);
$trail = trim(fread($fh, 8192));
fclose($fh);
echo "before-halt\n", $trail, "\n";
__halt_compiler();
TRAILING-DATA-12345
PHPC;
file_put_contents($file, $code);
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
before-halt
TRAILING-DATA-12345
13 changes: 13 additions & 0 deletions Zend/tests/phpc/007_closing_tag.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
.phpc: ?> in a .phpc file drops to inline output, mirroring .php semantics
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, "echo \"from-phpc\\n\";\n?>\nplain inline content\n<?php echo \"back\\n\";");
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
from-phpc
plain inline content
back
12 changes: 12 additions & 0 deletions Zend/tests/phpc/008_empty.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
--TEST--
.phpc: an empty file produces no output and no error
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, '');
register_shutdown_function(fn() => @unlink($file));
require $file;
echo "after-require\n";
?>
--EXPECT--
after-require
11 changes: 11 additions & 0 deletions Zend/tests/phpc/009_declare_strict_types.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
.phpc: declare(strict_types=1) works as first statement in a .phpc file
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, "declare(strict_types=1);\nfunction add(int \$a, int \$b): int { return \$a + \$b; }\necho add(1, 2), \"\\n\";");
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
3
20 changes: 20 additions & 0 deletions Zend/tests/phpc/010_class_definition.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
.phpc: class definitions and namespaces work in a .phpc file
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
file_put_contents($file, <<<'PHPC'
namespace App\Demo;

class Greeter {
public function __construct(private string $who) {}
public function hello(): string { return "hello, {$this->who}"; }
}

echo (new Greeter('world'))->hello(), "\n";
PHPC);
register_shutdown_function(fn() => @unlink($file));
require $file;
?>
--EXPECT--
hello, world
11 changes: 11 additions & 0 deletions Zend/tests/phpc/011_eval_unchanged.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
.phpc: eval() is unaffected — string compile path is independent of file extension
--FILE--
<?php
/* eval() always uses ZEND_COMPILE_POSITION_AFTER_OPEN_TAG, which already
* starts in ST_IN_SCRIPTING. .phpc semantics live in the file-compile
* path and must not leak into the string-compile path. */
eval('echo "eval-ok\n";');
?>
--EXPECT--
eval-ok
16 changes: 16 additions & 0 deletions Zend/tests/phpc/012_token_get_all_unchanged.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
.phpc: token_get_all() string path is unaffected (still requires <?php)
--FILE--
<?php
$tokens = token_get_all("<?php echo 1;");
foreach ($tokens as $t) {
if (is_array($t)) {
echo token_name($t[0]), "\n";
}
}
?>
--EXPECT--
T_OPEN_TAG
T_ECHO
T_WHITESPACE
T_LNUMBER
17 changes: 17 additions & 0 deletions Zend/tests/phpc/013_shebang_main_script.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
--TEST--
.phpc: a CLI shebang line at the top of a .phpc main script is silently skipped
--FILE--
<?php
$dir = sys_get_temp_dir();
$file = $dir . '/' . basename(__FILE__, '.php') . '_main.phpc';
file_put_contents($file,
"#!/usr/bin/env php\necho \"shebang-skipped\\n\"; echo __LINE__, \"\\n\";");
register_shutdown_function(fn() => @unlink($file));

$php = PHP_BINARY;
$out = shell_exec(escapeshellarg($php) . ' -n ' . escapeshellarg($file));
echo $out;
?>
--EXPECT--
shebang-skipped
2
18 changes: 18 additions & 0 deletions Zend/tests/phpc/014_phpc_with_open_tag.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
--TEST--
.phpc: an accidental literal '<?php' inside a .phpc file is a syntax error (not magic)
--FILE--
<?php
$file = __DIR__ . '/' . basename(__FILE__, '.php') . '.phpc';
/* In .phpc mode the lexer starts in ST_IN_SCRIPTING. A literal '<?php' at
* the top is now interpreted as code, not an opening tag, and is a
* syntax error. This guards against authors accidentally double-opening. */
file_put_contents($file, "<?php\necho \"this-must-fail\";\n");
register_shutdown_function(fn() => @unlink($file));
try {
require $file;
} catch (\ParseError $e) {
echo "ParseError: ", $e->getMessage(), "\n";
}
?>
--EXPECTF--
ParseError: syntax error, %s
23 changes: 23 additions & 0 deletions Zend/tests/phpc/015_php_with_phpc_substring.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
.phpc: extension match is strict; ".phpcc" or ".php" with "phpc" in middle are NOT .phpc
--FILE--
<?php
$dir = __DIR__;
$base = basename(__FILE__, '.php');

/* Filename containing "phpc" but not ending in ".phpc" must NOT trigger
* pure-PHP mode. */
$f1 = "$dir/{$base}_phpc.php";
file_put_contents($f1, 'echo "must-stay-inline";');
register_shutdown_function(fn() => @unlink($f1));
require $f1;
echo "\n";

$f2 = "$dir/{$base}.phpcc";
file_put_contents($f2, 'echo "must-stay-inline-too";');
register_shutdown_function(fn() => @unlink($f2));
require $f2;
?>
--EXPECT--
echo "must-stay-inline";
echo "must-stay-inline-too";
55 changes: 53 additions & 2 deletions Zend/zend_language_scanner.l
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,58 @@ ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle)
zend_error_noreturn(E_COMPILE_ERROR, "zend_stream_mmap() failed");
}

if (CG(skip_shebang)) {
/* Pure-code PHP files via .phpc extension (RFC: optional_php_tags).
*
* Files whose name ends in ".phpc" are parsed as pure PHP: the lexer
* starts directly in ST_IN_SCRIPTING, with no opening "<?php" required.
* A leading UTF-8 BOM and an optional CLI shebang line (#!.*\n) are
* silently skipped. The closing tag "?>" and __halt_compiler() retain
* their current semantics, so a .phpc file remains free to drop into
* inline output mode or terminate compilation, if desired.
*
* Files whose name does NOT end in ".phpc" (in particular: every .php
* file, every .phar entry, every stream wrapper without a .phpc name,
* every eval()/stdin/highlight invocation) take the historical code
* path unchanged. .phpc detection is purely additive.
*/
bool is_phpc_file = false;
{
zend_string *fname = file_handle->opened_path
? file_handle->opened_path : file_handle->filename;
if (fname && ZSTR_LEN(fname) >= sizeof(".phpc") - 1) {
const char *tail = ZSTR_VAL(fname) + ZSTR_LEN(fname) - (sizeof(".phpc") - 1);
if (memcmp(tail, ".phpc", sizeof(".phpc") - 1) == 0) {
is_phpc_file = true;
}
}
}

uint32_t phpc_start_lineno = 1;
if (is_phpc_file) {
unsigned char *p = (unsigned char *)buf;
unsigned char *limit = p + size;

/* Skip a UTF-8 BOM if present. */
if (limit - p >= 3 && p[0] == 0xEF && p[1] == 0xBB && p[2] == 0xBF) {
p += 3;
}

/* Skip a shebang line in CLI mode. .phpc files can be #!-scripts. */
if (CG(skip_shebang) && limit - p >= 2 && p[0] == '#' && p[1] == '!') {
while (p < limit && *p != '\n') {
p++;
}
if (p < limit) {
p++; /* consume the newline */
phpc_start_lineno = 2;
}
}

if (p != (unsigned char *)buf) {
SCNG(yy_cursor) = p;
}
BEGIN(ST_IN_SCRIPTING);
} else if (CG(skip_shebang)) {
BEGIN(SHEBANG);
} else {
BEGIN(INITIAL);
Expand All @@ -585,7 +636,7 @@ ZEND_API zend_result open_file_for_scanning(zend_file_handle *file_handle)
SCNG(on_event) = NULL;
SCNG(on_event_context) = NULL;
RESET_DOC_COMMENT();
CG(zend_lineno) = 1;
CG(zend_lineno) = phpc_start_lineno;
CG(increment_lineno) = 0;
return SUCCESS;
}
Expand Down
Loading