From 835b688fcf1727952b222bedbc1128d858265b46 Mon Sep 17 00:00:00 2001 From: PurHur Date: Fri, 22 May 2026 22:49:33 +0000 Subject: [PATCH] VM/JIT: runtime __DIR__ and __FILE__ via script stack (#707) Resolve __DIR__/__FILE__ at runtime from the executing script path instead of baking dirname at parse time only. Push/pop ScriptStack on TYPE_INCLUDE, add TYPE_SCRIPT_MAGIC opcode, and extend ConstStringFolder for lint folding. Closes #707. Documents rows in capabilities-syntax (#706). Co-authored-by: Cursor --- docs/capabilities-syntax.md | 2 + lib/Block.php | 37 +++++++++ lib/Compiler.php | 7 ++ lib/Frame.php | 3 + lib/JIT.php | 9 +++ lib/JIT/ScriptMagic.php | 24 ++++++ lib/OpCode.php | 6 ++ lib/OpCodeNames.php | 2 + lib/Runtime.php | 19 ++++- lib/VM.php | 47 ++++++++++-- lib/VM/Context.php | 3 + lib/VM/ScriptStack.php | 52 +++++++++++++ lib/Web/ConstStringFolder.php | 76 ++++++++++++++++++- patches/php-cfg-magic-script-const.patch | 52 +++++++++++++ patches/php-types-magic-script-const.patch | 12 +++ script/apply-patches.sh | 8 ++ script/capability-syntax-lib.php | 16 ++++ .../cases/language/magic_dir_nested.phpt | 7 ++ .../cases/language/magic_dir_nested/entry.php | 6 ++ .../language/magic_dir_nested/sub/helper.php | 8 ++ .../cases/language/magic_dir_nested_jit.phpt | 7 ++ 21 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 lib/JIT/ScriptMagic.php create mode 100644 lib/VM/ScriptStack.php create mode 100644 patches/php-cfg-magic-script-const.patch create mode 100644 patches/php-types-magic-script-const.patch create mode 100644 test/compliance/cases/language/magic_dir_nested.phpt create mode 100644 test/compliance/cases/language/magic_dir_nested/entry.php create mode 100644 test/compliance/cases/language/magic_dir_nested/sub/helper.php create mode 100644 test/compliance/cases/language/magic_dir_nested_jit.phpt diff --git a/docs/capabilities-syntax.md b/docs/capabilities-syntax.md index 2c12ab9d..b40f57cd 100644 --- a/docs/capabilities-syntax.md +++ b/docs/capabilities-syntax.md @@ -19,6 +19,8 @@ Tracking issues: [#58](https://github.com/PurHur/php-compiler/issues/58), [#145] | Arrow functions `fn () =>` | no | no | no | [#142](https://github.com/PurHur/php-compiler/issues/142) | | | Magic constants `__CLASS__`, `__METHOD__`, `__FUNCTION__` | yes | yes | yes | [#199](https://github.com/PurHur/php-compiler/issues/199) | Lowered at parse time via php-cfg MagicStringResolver; compliance PHPT | | Magic constant `__NAMESPACE__` | yes | yes | yes | [#199](https://github.com/PurHur/php-compiler/issues/199) | Requires `namespace` declaration (#84); compliance PHPT | +| Magic constants `__DIR__`, `__FILE__` | yes | yes | yes | [#707](https://github.com/PurHur/php-compiler/issues/707) | VM script stack on include; JIT uses per-unit script path | +| Magic constant `__LINE__` | no | no | no | [#707](https://github.com/PurHur/php-compiler/issues/707) | Lowered at parse time via php-cfg MagicStringResolver (best effort) | _Syntax AOT column reflects `Runtime::MODE_AOT` compile probes unless a row pins AOT (e.g. #568 native link)._ ## Web north-star (`examples/003-MiniWebApp`) diff --git a/lib/Block.php b/lib/Block.php index d5d082e8..8bdca952 100755 --- a/lib/Block.php +++ b/lib/Block.php @@ -12,12 +12,14 @@ // used as a property type. // @phan-suppress-next-line PhanUnreferencedUseNormal use PHPCfg\Func; +use PHPCfg\Op; use PHPCfg\Block as CfgBlock; use PHPCfg\Operand; use PHPCfg\Operand\Literal; use PHPCfg\Operand\Temporary; use PHPCfg\Operand\Variable as VarOperand; use PHPCompiler\VM\Context; +use PHPCompiler\VM\ScriptStack; use PHPCompiler\VM\Variable; use PHPCompiler\Web\Superglobals; @@ -69,12 +71,46 @@ class Block { */ public array $deployIncludePaths = []; + /** Absolute entry script path when CFG filename attribute is missing (issue #707). */ + private string $scriptPathOverride = ''; + public function __construct(?CfgBlock $block) { $this->orig = $block; $this->scope = new \SplObjectStorage; $this->args = new \SplObjectStorage; } + public function setScriptPath(string $path): void + { + $this->scriptPathOverride = ScriptStack::normalize($path); + } + + /** Absolute path of the PHP source unit for this block (issue #707). */ + public function scriptPath(): string + { + if ('' !== $this->scriptPathOverride) { + return $this->scriptPathOverride; + } + if (null !== $this->func) { + $file = $this->func->getFile(); + if ('' !== $file && 'unknown' !== $file) { + return ScriptStack::normalize($file); + } + } + if (null !== $this->orig) { + foreach ($this->orig->children as $child) { + if ($child instanceof Op) { + $file = $child->getFile(); + if ('' !== $file && 'unknown' !== $file) { + return ScriptStack::normalize($file); + } + } + } + } + + return ''; + } + public function getOperand(int $offset): Operand { foreach ($this->scope as $operand) { if ($this->scope[$operand] === $offset) { @@ -285,6 +321,7 @@ public function getFrame(Context $context, ?Frame $frame = null): Frame { } $return = new Frame(null, $this, $frame, ...$scope); + $return->scriptPath = $this->scriptPath(); if (!is_null($frame) && !is_null($frame->returnVar)) { $return->returnVar = $frame->returnVar; } diff --git a/lib/Compiler.php b/lib/Compiler.php index 21142770..55e0cd36 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -763,6 +763,13 @@ protected function compileExpr(Op\Expr $expr, Block $block): array { ); } return $return; + case Op\Expr\MagicScriptConst::class: + return [new OpCode( + OpCode::TYPE_SCRIPT_MAGIC, + $this->compileOperand($expr->result, $block, false), + null, + $expr->kind, + )]; case Op\Expr\Include_::class: return [$this->compileIncludeOp($expr, $block)]; case Op\Expr\Isset_::class: diff --git a/lib/Frame.php b/lib/Frame.php index 77543703..d658d276 100755 --- a/lib/Frame.php +++ b/lib/Frame.php @@ -30,6 +30,9 @@ class Frame { /** When true, finishing this frame resumes the caller instead of ending execution. */ public bool $ephemeral = false; + /** Absolute path of the script this frame executes (issue #707). */ + public string $scriptPath = ''; + /** VM context for nested builtin calls (set when invoking Internal handlers). */ public ?Context $vmContext = null; diff --git a/lib/JIT.php b/lib/JIT.php index 0a7ccd18..72a1a45a 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -445,6 +445,15 @@ private function compileBlockInternal( $value = JIT\IteratorHelper::compileValue($this->context, $array); $this->assignOperand($block->getOperand($op->arg1), $value); break; + case OpCode::TYPE_SCRIPT_MAGIC: + $magicStr = JIT\ScriptMagic::stringForBlock($block, (int) $op->arg3); + $lit = new Operand\Literal($magicStr); + $lit->type = \PHPTypes\Type::string(); + $this->assignOperand( + $block->getOperand($op->arg1), + JIT\Variable::fromLiteral($this->context, $lit) + ); + break; case OpCode::TYPE_INCLUDE: JIT\IncludeHelper::compileLiteral( $this, diff --git a/lib/JIT/ScriptMagic.php b/lib/JIT/ScriptMagic.php new file mode 100644 index 00000000..5e54b796 --- /dev/null +++ b/lib/JIT/ScriptMagic.php @@ -0,0 +1,24 @@ +scriptPath(); + if (OpCode::SCRIPT_MAGIC_DIR === $kind) { + return '' !== $path ? dirname($path) : ''; + } + + return $path; + } +} diff --git a/lib/OpCode.php b/lib/OpCode.php index 01e32530..8de02161 100755 --- a/lib/OpCode.php +++ b/lib/OpCode.php @@ -94,6 +94,12 @@ class OpCode { const TYPE_CATCH = 93; const TYPE_FINALLY = 94; const TYPE_DECLARE_GLOBAL_CONST = 95; + /** Runtime __DIR__ / __FILE__ from script stack (issue #707). arg3 = SCRIPT_MAGIC_* kind. */ + const TYPE_SCRIPT_MAGIC = 96; + + public const SCRIPT_MAGIC_DIR = 1; + + public const SCRIPT_MAGIC_FILE = 2; public int $type; public ?int $arg1; diff --git a/lib/OpCodeNames.php b/lib/OpCodeNames.php index c4fcdfd5..aeeb2433 100644 --- a/lib/OpCodeNames.php +++ b/lib/OpCodeNames.php @@ -180,6 +180,8 @@ function opcode_type_name(int $type): string return 'TYPE_FINALLY'; case 95: return 'TYPE_DECLARE_GLOBAL_CONST'; + case 96: + return 'TYPE_SCRIPT_MAGIC'; default: return 'unknown opcode'; } diff --git a/lib/Runtime.php b/lib/Runtime.php index aec29ead..3fe62467 100755 --- a/lib/Runtime.php +++ b/lib/Runtime.php @@ -159,11 +159,26 @@ public function standalone(?Block $block, string $outfile) { } public function parseAndCompile(string $code, string $filename): ?Block { - return $this->compile($this->parse($code, $filename)); + $block = $this->compile($this->parse($code, $filename)); + if (null !== $block) { + $block->setScriptPath($filename); + } + + return $block; } public function parseAndCompileFile(string $filename): ?Block { - return $this->compile($this->parse(file_get_contents($filename), $filename)); + $normalized = VM\ScriptStack::normalize($filename); + if ('' !== $normalized) { + $filename = $normalized; + } + + $block = $this->compile($this->parse(file_get_contents($filename), $filename)); + if (null !== $block) { + $block->setScriptPath($filename); + } + + return $block; } /** diff --git a/lib/VM.php b/lib/VM.php index 6418a9fa..14ec8f98 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -30,13 +30,29 @@ public function __construct(Context $context) { public function run(Block $block): int { if (!is_null($block->handler)) { - $block->handler->execute($block->getFrame($this->context)); + $frame = $block->getFrame($this->context); + $this->seedScriptPath($frame); + $block->handler->execute($frame); return self::SUCCESS; } - $this->context->push($block->getFrame($this->context)); + $frame = $block->getFrame($this->context); + $this->seedScriptPath($frame); + $this->context->push($frame); + + $result = $this->runFrames(); + if ('' !== $frame->scriptPath) { + $this->context->scriptStack->pop(); + } - return $this->runFrames(); + return $result; + } + + private function seedScriptPath(Frame $frame): void + { + if ('' !== $frame->scriptPath) { + $this->context->scriptStack->push($frame->scriptPath); + } } private function runFrames(): int @@ -460,9 +476,28 @@ private function runFrames(): int && Variable::TYPE_NULL !== $value->type ); break; + case OpCode::TYPE_SCRIPT_MAGIC: + $script = '' !== $frame->scriptPath + ? $frame->scriptPath + : $this->context->scriptStack->current(); + if ('' === $script) { + return $this->raise('__DIR__/__FILE__ used without script context', $frame); + } + $dst = $frame->scope[$op->arg1]; + if (OpCode::SCRIPT_MAGIC_DIR === $op->arg3) { + $dst->string(dirname($script)); + } else { + $dst->string($script); + } + break; case OpCode::TYPE_INCLUDE: $file = $frame->scope[$op->arg1]->toString(); - $parsed = $this->context->runtime->parseAndCompileFile($file); + $resolved = VM\ScriptStack::normalize($file); + if ('' === $resolved || !is_file($resolved)) { + return $this->raise('Failed opening required \''.$file.'\' for inclusion', $frame); + } + $this->context->scriptStack->push($resolved); + $parsed = $this->context->runtime->parseAndCompileFile($resolved); $new = $parsed->getFrame($this->context, $frame); $new->ephemeral = true; $new->parent = $frame; @@ -530,6 +565,7 @@ private function runFrames(): int } } if ($frame->ephemeral) { + $this->context->scriptStack->pop(); if (null !== $frame->parent) { $frame = $frame->parent; goto restart; @@ -541,7 +577,8 @@ private function runFrames(): int protected function raise(string $message, Frame $frame): int { - throw new \LogicException($message.' in '.$frame->block->getName()); + $where = '' !== $frame->scriptPath ? $frame->scriptPath : 'script'; + throw new \LogicException($message.' in '.$where); } protected function defineClass(ClassEntry $entry, Block $block): void { diff --git a/lib/VM/Context.php b/lib/VM/Context.php index 569f368c..f1996fef 100755 --- a/lib/VM/Context.php +++ b/lib/VM/Context.php @@ -27,9 +27,12 @@ class Context { public ErrorReporter $errors; + public ScriptStack $scriptStack; + public function __construct(Runtime $runtime) { $this->runtime = $runtime; $this->errors = new ErrorReporter(); + $this->scriptStack = new ScriptStack(); } public function constantFetch(string $name): ?Variable { diff --git a/lib/VM/ScriptStack.php b/lib/VM/ScriptStack.php new file mode 100644 index 00000000..ba29b503 --- /dev/null +++ b/lib/VM/ScriptStack.php @@ -0,0 +1,52 @@ + absolute script paths */ + private array $stack = []; + + public function push(string $scriptPath): void + { + $normalized = self::normalize($scriptPath); + if ('' === $normalized) { + return; + } + $this->stack[] = $normalized; + } + + public function pop(): void + { + if ([] !== $this->stack) { + array_pop($this->stack); + } + } + + public function current(): string + { + if ([] === $this->stack) { + return ''; + } + + return $this->stack[count($this->stack) - 1]; + } + + public static function normalize(string $path): string + { + if ('' === $path) { + return ''; + } + $resolved = realpath($path); + if (false !== $resolved) { + return $resolved; + } + + return $path; + } +} diff --git a/lib/Web/ConstStringFolder.php b/lib/Web/ConstStringFolder.php index 4fe367bd..a00d88fe 100644 --- a/lib/Web/ConstStringFolder.php +++ b/lib/Web/ConstStringFolder.php @@ -22,6 +22,10 @@ final class ConstStringFolder { public static function fold(Operand $operand, string $sourceFile = ''): ?string { + $magic = self::magicScriptConstValue($operand, $sourceFile, null); + if (null !== $magic) { + return $magic; + } $literal = self::literalStringValue($operand); if (null !== $literal) { return $literal; @@ -29,7 +33,7 @@ public static function fold(Operand $operand, string $sourceFile = ''): ?string if ($operand instanceof Operand\Temporary) { $original = $operand->original; if ($original instanceof Op\Expr\BinaryOp\Concat) { - return self::foldConcat($original, $sourceFile); + return self::foldConcat($original, $sourceFile, null); } if ($original instanceof Operand\Literal && is_string($original->value)) { return $original->value; @@ -39,7 +43,7 @@ public static function fold(Operand $operand, string $sourceFile = ''): ?string return null; } - public static function foldConcat(Op\Expr\BinaryOp\Concat $concat, string $sourceFile = ''): ?string + public static function foldConcat(Op\Expr\BinaryOp\Concat $concat, string $sourceFile = '', ?CfgBlock $cfg = null): ?string { $left = self::fold($concat->left, $sourceFile); $right = self::fold($concat->right, $sourceFile); @@ -55,6 +59,10 @@ public static function foldConcat(Op\Expr\BinaryOp\Concat $concat, string $sourc if (null !== $leftLit && null !== $rightLit) { return $leftLit.$rightLit; } + $leftMagic = self::magicScriptConstValue($concat->left, $sourceFile, $cfg); + if (null !== $leftMagic && null !== $rightLit) { + return $leftMagic.$rightLit; + } if (null !== $rightLit && $rightLit === $dir && null !== $leftLit) { return $leftLit.$dir; } @@ -247,7 +255,7 @@ public static function foldForInclude(CfgBlock $cfg, Operand $operand, string $s if ($operand instanceof Operand\Temporary) { $concat = self::findConcatForOperand($cfg, $operand); if (null !== $concat) { - return self::foldConcat($concat, $sourceFile); + return self::foldConcat($concat, $sourceFile, $cfg); } } @@ -298,10 +306,72 @@ private static function literalStringValue(Operand $operand): ?string if ($operand instanceof Operand\Literal && is_string($operand->value)) { return $operand->value; } + $magic = self::magicScriptConstValue($operand); + if (null !== $magic) { + return $magic; + } + + return null; + } + + private static function magicScriptConstValue(Operand $operand, string $sourceFile = '', ?CfgBlock $cfg = null): ?string + { + $magic = null; + if ($operand instanceof Op\Expr\MagicScriptConst) { + $magic = $operand; + } elseif ($operand instanceof Operand\Temporary && $operand->original instanceof Op\Expr\MagicScriptConst) { + $magic = $operand->original; + } elseif (null !== $cfg) { + $magic = self::findMagicScriptConstForOperand($cfg, $operand); + } + if (null === $magic || '' === $sourceFile) { + return null; + } + if (Op\Expr\MagicScriptConst::KIND_DIR === $magic->kind) { + return self::sourceDir($sourceFile); + } + if (Op\Expr\MagicScriptConst::KIND_FILE === $magic->kind) { + $resolved = realpath($sourceFile); + + return false !== $resolved ? $resolved : $sourceFile; + } + + return null; + } + + private static function findMagicScriptConstForOperand(CfgBlock $cfg, Operand $operand): ?Op\Expr\MagicScriptConst + { + foreach (self::collectMagicScriptConsts($cfg) as $magic) { + if ($magic->result === $operand) { + return $magic; + } + } return null; } + /** + * @return list + */ + private static function collectMagicScriptConsts(CfgBlock $block): array + { + $found = []; + foreach ($block->children as $child) { + if ($child instanceof Op\Expr\MagicScriptConst) { + $found[] = $child; + } + foreach ($child->getSubBlocks() as $sub) { + if ($sub instanceof CfgBlock) { + foreach (self::collectMagicScriptConsts($sub) as $nested) { + $found[] = $nested; + } + } + } + } + + return $found; + } + private static function sourceDir(string $sourceFile): string { $resolved = realpath($sourceFile); diff --git a/patches/php-cfg-magic-script-const.patch b/patches/php-cfg-magic-script-const.patch new file mode 100644 index 00000000..1f954285 --- /dev/null +++ b/patches/php-cfg-magic-script-const.patch @@ -0,0 +1,52 @@ +--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/MagicScriptConst.php ++++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/MagicScriptConst.php +@@ -0,0 +1,31 @@ ++kind = $kind; ++ } ++ ++ public function getVariableNames(): array ++ { ++ return ['result']; ++ } ++} +--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php ++++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php +@@ -1455,10 +1455,14 @@ class Parser + case 'Scalar_MagicConst_Class': + // TODO + return new Literal('__CLASS__'); + case 'Scalar_MagicConst_Dir': +- return new Literal(dirname($this->fileName)); ++ $op = new Op\Expr\MagicScriptConst(Op\Expr\MagicScriptConst::KIND_DIR, $this->mapAttributes($scalar)); ++ $this->block->children[] = $op; ++ return $op->result; + case 'Scalar_MagicConst_File': +- return new Literal($this->fileName); ++ $op = new Op\Expr\MagicScriptConst(Op\Expr\MagicScriptConst::KIND_FILE, $this->mapAttributes($scalar)); ++ $this->block->children[] = $op; ++ return $op->result; + case 'Scalar_MagicConst_Namespace': + // TODO + return new Literal('__NAMESPACE__'); diff --git a/patches/php-types-magic-script-const.patch b/patches/php-types-magic-script-const.patch new file mode 100644 index 00000000..eaa3c6f1 --- /dev/null +++ b/patches/php-types-magic-script-const.patch @@ -0,0 +1,12 @@ +--- vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php ++++ vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php +@@ -281,6 +281,8 @@ class TypeReconstructor + case 'Expr_Yield': + case 'Expr_Include': + // TODO: we may be able to determine these... + return false; ++ case 'Expr_MagicScriptConst': ++ return [Type::string()]; + } + + throw new \LogicException('Unknown variable op found: '.$op->getType()); diff --git a/script/apply-patches.sh b/script/apply-patches.sh index 957b0f55..9eda9642 100755 --- a/script/apply-patches.sh +++ b/script/apply-patches.sh @@ -65,6 +65,12 @@ patch_already_applied() { php-cfg-magic-constants.patch) grep -q 'namespaceStack' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/AstVisitor/MagicStringResolver.php" 2>/dev/null ;; + php-cfg-magic-script-const.patch) + [[ -f "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Op/Expr/MagicScriptConst.php" ]] + ;; + php-types-magic-script-const.patch) + grep -q "case 'Expr_MagicScriptConst':" "$ROOT/vendor/ircmaxell/php-types/lib/PHPTypes/TypeReconstructor.php" 2>/dev/null + ;; *) return 1 ;; @@ -112,6 +118,7 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-cfg" ]]; then apply_patch "$PATCH_DIR/php-cfg-strict-types.patch" apply_patch "$PATCH_DIR/php-cfg-trycatch.patch" apply_patch "$PATCH_DIR/php-cfg-magic-constants.patch" + apply_patch "$PATCH_DIR/php-cfg-magic-script-const.patch" fi if [[ -d "$ROOT/vendor/ircmaxell/php-types" ]]; then @@ -132,6 +139,7 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-types" ]]; then apply_patch "$PATCH_DIR/php-types-generics-fallback.patch" apply_patch "$PATCH_DIR/php-types-generics-list-array.patch" apply_patch "$PATCH_DIR/php-types-ns-func-call.patch" + apply_patch "$PATCH_DIR/php-types-magic-script-const.patch" fi if [[ -d "$ROOT/vendor/pre/plugin" ]]; then diff --git a/script/capability-syntax-lib.php b/script/capability-syntax-lib.php index c127fee0..4610d53e 100644 --- a/script/capability-syntax-lib.php +++ b/script/capability-syntax-lib.php @@ -103,6 +103,22 @@ function syntaxRowDefinitions(): array 'notes' => ['Requires `namespace` declaration (#84)'], 'probe' => null, ], + [ + 'id' => 'magic_const_dir_file', + 'construct' => 'Magic constants `__DIR__`, `__FILE__`', + 'opcodes' => ['TYPE_SCRIPT_MAGIC', 'TYPE_INCLUDE'], + 'issue' => 707, + 'notes' => ['VM script stack on include; JIT uses per-unit script path'], + 'probe' => null, + ], + [ + 'id' => 'magic_const_line', + 'construct' => 'Magic constant `__LINE__`', + 'opcodes' => [], + 'issue' => 707, + 'notes' => ['Lowered at parse time via php-cfg MagicStringResolver (best effort)'], + 'probe' => null, + ], ]; } diff --git a/test/compliance/cases/language/magic_dir_nested.phpt b/test/compliance/cases/language/magic_dir_nested.phpt new file mode 100644 index 00000000..86b8ab9f --- /dev/null +++ b/test/compliance/cases/language/magic_dir_nested.phpt @@ -0,0 +1,7 @@ +--TEST-- +Language: __DIR__ in included helper matches helper directory (#707, #85) +--RUNFILE-- +magic_dir_nested/entry.php +--EXPECTREGEX-- +#/sub\s*$ +-- diff --git a/test/compliance/cases/language/magic_dir_nested/entry.php b/test/compliance/cases/language/magic_dir_nested/entry.php new file mode 100644 index 00000000..c3d3531e --- /dev/null +++ b/test/compliance/cases/language/magic_dir_nested/entry.php @@ -0,0 +1,6 @@ +