From 9c45bf883a0d4e6491df67e94a02182bf90d2b1e Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 13 Aug 2026 07:58:35 +0000 Subject: [PATCH] =?UTF-8?q?Stdlib:=20stream=5Fcontext=5Fset=5Foption=20inc?= =?UTF-8?q?omplete=20string=20form=20=E2=86=92=20ValueError=20(#30645)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match php-src streamsfuncs.c overload guards: 2-arg/3-arg string form throws Zend ValueError instead of LogicException or silent true. Co-authored-by: Cursor --- docs/bootstrap-inventory.md | 47 ++-- ext/standard/JitStreamContextSetOption.php | 215 +++++++++++++++--- ext/standard/VmStreamContext.php | 55 ++++- ext/standard/stream_context_set_option.php | 20 +- phpunit.xml.dist | 2 + ...ContextSetOptionIncomplete30645JITTest.php | 36 +++ ...mContextSetOptionIncomplete30645VMTest.php | 27 +++ ...m_context_set_option_incomplete_30645.phpt | 33 +++ ...p_stream_context_set_option_incomplete.php | 16 ++ ...treamContextSetOptionIncompleteAotTest.php | 71 ++++++ ...45StreamContextSetOptionIncompleteTest.php | 38 ++++ 11 files changed, 492 insertions(+), 68 deletions(-) create mode 100644 test/compliance/StreamContextSetOptionIncomplete30645JITTest.php create mode 100644 test/compliance/StreamContextSetOptionIncomplete30645VMTest.php create mode 100644 test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt create mode 100644 test/repro/maintainer_gap_stream_context_set_option_incomplete.php create mode 100644 test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php create mode 100644 test/unit/Issue30645StreamContextSetOptionIncompleteTest.php diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 7fdc44685e4..b08d935e09d 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -12,7 +12,7 @@ Regenerate: `php script/bootstrap-inventory.php` | Phase A inventory files (M2 ratio SSOT) | 7381 | | Phase A ratio-deferred paths | 0 | | Source constructs flagged (blockers) | 0 | -| Source constructs flagged (warnings) | 23918 | +| Source constructs flagged (warnings) | 23917 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -4493,7 +4493,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag | `ext/standard/stream_context_get_options.php` | 0 | 1 | | `ext/standard/stream_context_get_params.php` | 0 | 1 | | `ext/standard/stream_context_set_default.php` | 0 | 1 | -| `ext/standard/stream_context_set_option.php` | 0 | 3 | +| `ext/standard/stream_context_set_option.php` | 0 | 2 | | `ext/standard/stream_context_set_options.php` | 0 | 2 | | `ext/standard/stream_context_set_params.php` | 0 | 2 | | `ext/standard/stream_copy_to_stream.php` | 0 | 1 | @@ -29637,7 +29637,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/JitFdiv.php` **Warnings** (review for bootstrap subset): -- 15 class method(s) +- 17 class method(s) ### `ext/standard/JitFeof.php` @@ -31404,7 +31404,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/JitStreamContextSetOption.php` **Warnings** (review for bootstrap subset): -- 4 class method(s) +- 15 class method(s) ### `ext/standard/JitStreamContextSetOptions.php` @@ -36201,19 +36201,19 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag - new HashTable (line 127) - new Variable (line 129) - new Variable (line 307) -- new HashTable (line 367) -- new Variable (line 368) -- new Variable (line 384) -- new Variable (line 521) -- new HashTable (line 541) -- new Variable (line 550) -- new HashTable (line 567) -- new Variable (line 569) -- new HashTable (line 579) -- new Variable (line 581) -- new Variable (line 601) -- new HashTable (line 606) -- new Variable (line 608) +- new HashTable (line 408) +- new Variable (line 409) +- new Variable (line 425) +- new Variable (line 562) +- new HashTable (line 582) +- new Variable (line 591) +- new HashTable (line 608) +- new Variable (line 610) +- new HashTable (line 620) +- new Variable (line 622) +- new Variable (line 642) +- new HashTable (line 647) +- new Variable (line 649) - 28 class method(s) ### `ext/standard/VmStreamContextOptions.php` @@ -37851,13 +37851,13 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/diskfreespace.php` **Warnings** (review for bootstrap subset): -- new disk_free_space (line 23) +- new disk_free_space (line 24) - 2 class method(s) ### `ext/standard/disktotalspace.php` **Warnings** (review for bootstrap subset): -- new disk_total_space (line 23) +- new disk_total_space (line 24) - 2 class method(s) ### `ext/standard/dl.php` @@ -40833,8 +40833,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/standard/stream_context_set_option.php` **Warnings** (review for bootstrap subset): -- new Variable (line 65) -- 3 class method(s) +- 2 class method(s) - 1 closure(s) ### `ext/standard/stream_context_set_options.php` @@ -43453,7 +43452,7 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `ext/xml/xml_error_string.php` **Warnings** (review for bootstrap subset): -- new ArgumentCountError (line 26) +- new ArgumentCountError (line 31) - 2 class method(s) ### `ext/xml/xml_get_current_byte_index.php` @@ -45870,8 +45869,8 @@ Rank live CFG gaps across inventory files: `php script/bootstrap-inventory-triag ### `lib/BuiltinParamNames.php` **Warnings** (review for bootstrap subset): -- new ArgumentCountError (line 2393) -- new ArgumentCountError (line 2404) +- new ArgumentCountError (line 2396) +- new ArgumentCountError (line 2407) - 19 class method(s) - 1 closure(s) diff --git a/ext/standard/JitStreamContextSetOption.php b/ext/standard/JitStreamContextSetOption.php index 8c6f92e7b70..8854be86745 100644 --- a/ext/standard/JitStreamContextSetOption.php +++ b/ext/standard/JitStreamContextSetOption.php @@ -4,16 +4,22 @@ namespace PHPCompiler\ext\standard; +use PHPCompiler\JIT\BasicBlockHelper; use PHPCompiler\JIT\Builtin\StreamContextRuntime; use PHPCompiler\JIT\Context; +use PHPCompiler\JIT\ExceptionBridge; use PHPCompiler\JIT\HashTableHelper; use PHPCompiler\JIT\JitValueBox; use PHPCompiler\JIT\Variable as JITVariable; +use PHPCompiler\VM\Variable as VmVariable; +use PHPLLVM\Builder; use PHPLLVM\Value; -/** LLVM lowering for stream_context_set_option() (#3448). */ +/** LLVM lowering for stream_context_set_option() (#3448, #30645). */ final class JitStreamContextSetOption { + private static int $guardSeq = 0; + public static function invoke(Context $context, JITVariable ...$args): Value { // Arity checked by stream_context_set_option::call via requireArgCountRangeJit (#30584). @@ -23,40 +29,197 @@ public static function invoke(Context $context, JITVariable ...$args): Value JitStreamContextRequiredArg::validate($context, $args[0], 'stream_context_set_option', 1); - $ctxHt = self::loadContextArray($context, $args[0]); - if (2 === $argc) { - $optHt = self::loadOptionsArray($context, $args[1], 2); - $context->builder->call( - $context->lookupFunction('__phpc_stream_context_merge_options'), - $ctxHt, - $optHt - ); - } else { - // 3 or 4 args: singular form; omitted value is null (#30584 / php-src stub). - $wrapperVal = self::loadValuePointer($context, $args[1], 2); - $optionVal = self::loadValuePointer($context, $args[2], 3); - if (4 === $argc) { - $valueVal = self::loadValuePointer($context, $args[3], 4); - } else { - $nullSlot = JitValueBox::alloc($context); - $valueVal = JitValueBox::pointer($context, $nullSlot); - $context->builder->call($context->lookupFunction('__value__writeNull'), $valueVal); + $arg3 = $argc >= 3 ? $args[2] : null; + $arg4 = $argc >= 4 ? $args[3] : null; + $optionIsNull = self::operandIsNull($arg3); + $valueProvided = null !== $arg4; + + if (self::isCompileTimeArray($args[1])) { + if (!$optionIsNull) { + return self::emitValueError($context, VmStreamContext::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY); } - $context->builder->call( - $context->lookupFunction('__phpc_stream_context_set_single_option'), - $ctxHt, - $wrapperVal, - $optionVal, - $valueVal - ); + if ($valueProvided) { + return self::emitValueError($context, VmStreamContext::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY); + } + + return self::emitMergeOptions($context, $args[0], $args[1]); + } + + if (self::isCompileTimeNonArray($args[1])) { + if ($optionIsNull) { + return self::emitValueError($context, VmStreamContext::SET_OPTION_OPTION_NAME_NULL_ON_STRING); + } + if (!$valueProvided) { + return self::emitValueError($context, VmStreamContext::SET_OPTION_VALUE_REQUIRED_ON_STRING); + } + + return self::emitSetSingleOption($context, $args[0], $args[1], $args[2], $args[3]); } + return self::emitRuntimeWrapperDispatch( + $context, + $args[0], + $args[1], + $arg3, + $arg4, + $optionIsNull, + $valueProvided + ); + } + + private static function emitRuntimeWrapperDispatch( + Context $context, + JITVariable $ctxArg, + JITVariable $wrapperArg, + ?JITVariable $arg3, + ?JITVariable $arg4, + bool $optionIsNull, + bool $valueProvided + ): Value { + $tag = 'sctxso'.(string) ++self::$guardSeq; + $isArray = self::emitRuntimeIsArray($context, $wrapperArg); + $arrBb = BasicBlockHelper::append($context, 'sctx_set_opt_arr_'.$tag); + $strBb = BasicBlockHelper::append($context, 'sctx_set_opt_str_'.$tag); + $joinBb = BasicBlockHelper::append($context, 'sctx_set_opt_join_'.$tag); + $context->builder->branchIf($isArray, $arrBb, $strBb); + + $context->builder->positionAtEnd($arrBb); + if (!$optionIsNull) { + ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY); + BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_arr_opt_dead_'.$tag); + } elseif ($valueProvided) { + ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY); + BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_arr_val_dead_'.$tag); + } else { + self::emitMergeOptionsVoid($context, $ctxArg, $wrapperArg); + } + $context->builder->branch($joinBb); + + $context->builder->positionAtEnd($strBb); + if ($optionIsNull) { + ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_OPTION_NAME_NULL_ON_STRING); + BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_str_opt_dead_'.$tag); + } elseif (!$valueProvided) { + ExceptionBridge::emitValueErrorAndAbort($context, VmStreamContext::SET_OPTION_VALUE_REQUIRED_ON_STRING); + BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_str_val_dead_'.$tag); + } else { + assert(null !== $arg3 && null !== $arg4); + self::emitSetSingleOptionVoid($context, $ctxArg, $wrapperArg, $arg3, $arg4); + } + $context->builder->branch($joinBb); + + $context->builder->positionAtEnd($joinBb); + + return self::emitTrue($context); + } + + private static function emitRuntimeIsArray(Context $context, JITVariable $arg): Value + { + $ptr = self::loadValuePointer($context, $arg, 2); + $map = $context->structFieldMap['__value__']; + $typeByte = $context->builder->load( + $context->builder->structGep($ptr, $map['type']) + ); + $i8 = $context->getTypeFromString('int8'); + $kind = $context->builder->and($typeByte, $i8->constInt(0x7f, false)); + $isArray = $context->builder->icmp( + Builder::INT_EQ, + $kind, + $i8->constInt(VmVariable::TYPE_ARRAY, false) + ); + $isHt = $context->builder->icmp( + Builder::INT_EQ, + $kind, + $i8->constInt(JITVariable::TYPE_HASHTABLE & 0x7f, false) + ); + + return $context->builder->or($isArray, $isHt); + } + + private static function emitMergeOptions(Context $context, JITVariable $ctxArg, JITVariable $optArg): Value + { + self::emitMergeOptionsVoid($context, $ctxArg, $optArg); + + return self::emitTrue($context); + } + + private static function emitMergeOptionsVoid(Context $context, JITVariable $ctxArg, JITVariable $optArg): void + { + $ctxHt = self::loadContextArray($context, $ctxArg); + $optHt = self::loadOptionsArray($context, $optArg, 2); + $context->builder->call( + $context->lookupFunction('__phpc_stream_context_merge_options'), + $ctxHt, + $optHt + ); + } + + private static function emitSetSingleOption( + Context $context, + JITVariable $ctxArg, + JITVariable $wrapperArg, + JITVariable $optionArg, + JITVariable $valueArg + ): Value { + self::emitSetSingleOptionVoid($context, $ctxArg, $wrapperArg, $optionArg, $valueArg); + + return self::emitTrue($context); + } + + private static function emitSetSingleOptionVoid( + Context $context, + JITVariable $ctxArg, + JITVariable $wrapperArg, + JITVariable $optionArg, + JITVariable $valueArg + ): void { + $ctxHt = self::loadContextArray($context, $ctxArg); + $wrapperVal = self::loadValuePointer($context, $wrapperArg, 2); + $optionVal = self::loadValuePointer($context, $optionArg, 3); + $valueVal = self::loadValuePointer($context, $valueArg, 4); + $context->builder->call( + $context->lookupFunction('__phpc_stream_context_set_single_option'), + $ctxHt, + $wrapperVal, + $optionVal, + $valueVal + ); + } + + private static function emitValueError(Context $context, string $message): Value + { + ExceptionBridge::emitValueErrorAndAbort($context, $message); + BasicBlockHelper::ensureOpenInsertBlock($context, 'sctx_set_opt_ve_dead_'.(string) ++self::$guardSeq); + + return self::emitTrue($context); + } + + private static function emitTrue(Context $context): Value + { $slot = JitValueBox::alloc($context); JitValueBox::writeBool($context, $slot, $context->constantFromBool(true)); return JitValueBox::pointer($context, $slot); } + private static function isCompileTimeArray(JITVariable $arg): bool + { + return JITVariable::TYPE_HASHTABLE === $arg->type + || 0 !== ($arg->type & JITVariable::IS_NATIVE_ARRAY); + } + + private static function isCompileTimeNonArray(JITVariable $arg): bool + { + return !self::isCompileTimeArray($arg) && JITVariable::TYPE_VALUE !== $arg->type; + } + + private static function operandIsNull(?JITVariable $arg): bool + { + return null === $arg + || JITVariable::TYPE_NULL === $arg->type + || $arg->isNullConstant; + } + private static function loadContextArray(Context $context, JITVariable $arg): Value { if (JITVariable::TYPE_HASHTABLE === $arg->type) { diff --git a/ext/standard/VmStreamContext.php b/ext/standard/VmStreamContext.php index 476dc293a25..4686648e2e5 100644 --- a/ext/standard/VmStreamContext.php +++ b/ext/standard/VmStreamContext.php @@ -329,23 +329,64 @@ private static function canonicalHashTable(Variable $context): HashTable } /** - * stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448). + * php-src streamsfuncs.c zend_argument_value_error(3, "cannot be null when argument #2 … is a string") (#30645). + */ + public const SET_OPTION_OPTION_NAME_NULL_ON_STRING = + 'stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string'; + + /** + * php-src streamsfuncs.c zend_argument_value_error(4, "must be provided when argument #2 … is a string") (#30645). + */ + public const SET_OPTION_VALUE_REQUIRED_ON_STRING = + 'stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string'; + + /** + * php-src streamsfuncs.c zend_argument_value_error(3, "must be null when argument #2 … is an array") (#30645). + */ + public const SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY = + 'stream_context_set_option(): Argument #3 ($option_name) must be null when argument #2 ($wrapper_or_options) is an array'; + + /** + * php-src streamsfuncs.c zend_argument_value_error(4, "cannot be provided when argument #2 … is an array") (#30645). + */ + public const SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY = + 'stream_context_set_option(): Argument #4 ($value) cannot be provided when argument #2 ($wrapper_or_options) is an array'; + + /** + * stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448, #30645). + * + * Two-arg array form merges a full options array; four-arg string form sets one wrapper option. + * Incomplete string form / extra array-form args → Zend ValueError (not LogicException / silent true). * - * Two-arg form merges a full options array; four-arg form sets one wrapper option. + * $arg3/$arg4 PHP null means the argument was omitted; a Variable (including TYPE_NULL) was passed. */ public static function setOption(Variable $context, Variable $arg2, ?Variable $arg3 = null, ?Variable $arg4 = null): bool { $context = self::requireRepresentation($context, 'stream_context_set_option'); $resolved2 = $arg2->resolveIndirect(); - if (Variable::TYPE_ARRAY === $resolved2->type && (null === $arg3 || null === $arg4)) { + $optionIsNull = null === $arg3 || Variable::TYPE_NULL === $arg3->resolveIndirect()->type; + $valueProvided = null !== $arg4; + + if (Variable::TYPE_ARRAY === $resolved2->type) { + if (!$optionIsNull) { + throw new \ValueError(self::SET_OPTION_OPTION_NAME_MUST_BE_NULL_ON_ARRAY); + } + if ($valueProvided) { + throw new \ValueError(self::SET_OPTION_VALUE_FORBIDDEN_ON_ARRAY); + } + return self::setOptions($context, $arg2); } - if (null === $arg3 || null === $arg4) { - throw new \LogicException('stream_context_set_option(): missing wrapper/option/value arguments'); + + if ($optionIsNull) { + throw new \ValueError(self::SET_OPTION_OPTION_NAME_NULL_ON_STRING); + } + if (!$valueProvided) { + throw new \ValueError(self::SET_OPTION_VALUE_REQUIRED_ON_STRING); } - $wrapperName = self::coerceOptionKeyString($resolved2, 'stream_context_set_option', 2, 'wrapper'); - $optionName = self::coerceOptionKeyString($arg3->resolveIndirect(), 'stream_context_set_option', 3, 'option'); + $wrapperName = self::coerceOptionKeyString($resolved2, 'stream_context_set_option', 2, 'wrapper_or_options'); + $optionName = self::coerceOptionKeyString($arg3->resolveIndirect(), 'stream_context_set_option', 3, 'option_name'); $exportedValue = VmHttpBuildQuery::export($arg4); if (!\is_scalar($exportedValue) && null !== $exportedValue && !\is_array($exportedValue)) { $exportedValue = ''; diff --git a/ext/standard/stream_context_set_option.php b/ext/standard/stream_context_set_option.php index f8ba78d131e..36ad31f90e3 100644 --- a/ext/standard/stream_context_set_option.php +++ b/ext/standard/stream_context_set_option.php @@ -17,6 +17,7 @@ * stream_context_set_option() — singular or batch wrapper option write (ext/standard/streams.c, #3448). * * Excess/missing argc → Zend ArgumentCountError (#30584; php-src stub arity 2..4). + * Incomplete string form → Zend ValueError (#30645); do not synthesize a null $value. */ final class stream_context_set_option extends Internal { @@ -32,14 +33,19 @@ public function execute(Frame $frame): void $argc = \count($frame->calledArgs); if (2 === $argc) { $ok = VmStreamContext::setOption($frame->calledArgs[0], $frame->calledArgs[1]); + } elseif (3 === $argc) { + // Omit $value so setOption can distinguish missing arg #4 from an explicit null (#30645). + $ok = VmStreamContext::setOption( + $frame->calledArgs[0], + $frame->calledArgs[1], + $frame->calledArgs[2] + ); } else { - // 3 or 4: singular form; omitted value defaults to null like the stub. - $value = 4 === $argc ? $frame->calledArgs[3] : self::nullVar(); $ok = VmStreamContext::setOption( $frame->calledArgs[0], $frame->calledArgs[1], $frame->calledArgs[2], - $value + $frame->calledArgs[3] ); } BuiltinExecute::writeReturn( @@ -59,12 +65,4 @@ public function call(Context $context, JITVariable ...$args): Value return JitStreamContextSetOption::invoke($context, ...$args); } - - private static function nullVar(): Variable - { - $v = new Variable(); - $v->null(); - - return $v; - } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index dd79235fb59..b43d8aab5f4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -144,6 +144,8 @@ ./test/compliance/StreamFilterAppendPrependReflectionVMTest.php ./test/compliance/SocketReflectionStubsVMTest.php ./test/compliance/StreamContextSetOptionReturnTrueVMTest.php + ./test/compliance/StreamContextSetOptionIncomplete30645VMTest.php + ./test/compliance/StreamContextSetOptionIncomplete30645JITTest.php ./test/compliance/StreamSocketEnableCryptoReflectionVMTest.php ./test/compliance/HtmlDecodeFlagsNamedReflectionVMTest.php ./test/compliance/StreamIsattyReflectionVMTest.php diff --git a/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php b/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php new file mode 100644 index 00000000000..7cd160ed691 --- /dev/null +++ b/test/compliance/StreamContextSetOptionIncomplete30645JITTest.php @@ -0,0 +1,36 @@ + self::parsePHPT( + __DIR__.'/cases/stdlib/'.$file, + $file + ); + } + + public function setUp(): void + { + $this->BIN = realpath(__DIR__.'/../../bin/jit.php'); + if (!LlvmToolchain::hasLibrary(dirname(__DIR__, 2))) { + $this->markTestSkipped( + 'LLVM 9 toolchain not available. Run script/install-llvm9.sh or use the 22.04-dev Docker image.' + ); + } + } +} diff --git a/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php b/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php new file mode 100644 index 00000000000..09a797a1d97 --- /dev/null +++ b/test/compliance/StreamContextSetOptionIncomplete30645VMTest.php @@ -0,0 +1,27 @@ + self::parsePHPT( + __DIR__.'/cases/stdlib/'.$file, + $file + ); + } + + public function setUp(): void + { + $this->BIN = realpath(__DIR__.'/../../bin/vm.php'); + } +} diff --git a/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt b/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt new file mode 100644 index 00000000000..233ce3d656e --- /dev/null +++ b/test/compliance/cases/stdlib/stream_context_set_option_incomplete_30645.phpt @@ -0,0 +1,33 @@ +--TEST-- +stream_context_set_option string-form incomplete args → ValueError (#30645) +--FILE-- + stream_context_set_option($c, 'http'), + static fn () => stream_context_set_option($c, 'http', 'method'), + static fn () => stream_context_set_option($c, ['http' => ['timeout' => 1]], 'method'), +] as $fn) { + try { + $r = $fn(); + echo 'NO_THROW ', var_export($r, true), "\n"; + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } catch (Throwable $e) { + echo get_class($e), ':', $e->getMessage(), "\n"; + } +} +var_export(stream_context_set_option($c, ['http' => ['method' => 'GET']])); +echo "\n"; +var_export(stream_context_set_option($c, 'http', 'method', 'POST')); +echo "\n"; +$opts = stream_context_get_options($c); +echo $opts['http']['method'], "\n"; +?> +--EXPECT-- +stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string +stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string +stream_context_set_option(): Argument #3 ($option_name) must be null when argument #2 ($wrapper_or_options) is an array +true +true +POST diff --git a/test/repro/maintainer_gap_stream_context_set_option_incomplete.php b/test/repro/maintainer_gap_stream_context_set_option_incomplete.php new file mode 100644 index 00000000000..25b1a892bcb --- /dev/null +++ b/test/repro/maintainer_gap_stream_context_set_option_incomplete.php @@ -0,0 +1,16 @@ +getMessage(), "\n"; +} +try { + var_export(stream_context_set_option($c, 'http', 'method')); +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +var_export(stream_context_set_option($c, ['http' => ['method' => 'GET']])); +echo "\n"; +var_export(stream_context_set_option($c, 'http', 'method', 'GET')); +echo "\n"; diff --git a/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php b/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php new file mode 100644 index 00000000000..68317ee8537 --- /dev/null +++ b/test/unit/Issue30645StreamContextSetOptionIncompleteAotTest.php @@ -0,0 +1,71 @@ +markTestSkipped('LLVM 9 toolchain not available'); + } + $root = dirname(__DIR__, 2); + $src = sys_get_temp_dir().'/phpc_30645_'.getmypid().'.php'; + $bin = sys_get_temp_dir().'/phpc_30645_'.getmypid().'.bin'; + file_put_contents($src, <<<'PHP' +getMessage(), "\n"; +} +try { + stream_context_set_option($c, 'http', 'method'); + echo "NO_THROW\n"; +} catch (Throwable $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +var_export(stream_context_set_option($c, 'http', 'method', 'GET')); +echo "\n"; +PHP); + $compile = 'PHP_COMPILER_HELPER_RUNTIME_O=0 ' + .escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src).' 2>&1'; + exec($compile, $compileOut, $compileRc); + $this->assertSame(0, $compileRc, implode("\n", $compileOut)); + $this->assertFileExists($bin); + try { + $runOut = []; + exec(escapeshellarg($bin).' 2>&1', $runOut, $runRc); + $joined = implode("\n", $runOut); + $this->assertSame(0, $runRc, $joined); + $this->assertStringContainsString( + 'ValueError: stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string', + $joined + ); + $this->assertStringContainsString( + 'ValueError: stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string', + $joined + ); + $this->assertStringContainsString('true', $joined); + $this->assertStringNotContainsString('NO_THROW', $joined); + $this->assertStringNotContainsString('LogicException', $joined); + } finally { + @unlink($src); + @unlink($bin); + } + } +} diff --git a/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php b/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php new file mode 100644 index 00000000000..174b0bf9bcd --- /dev/null +++ b/test/unit/Issue30645StreamContextSetOptionIncompleteTest.php @@ -0,0 +1,38 @@ +assertNotFalse($code); + $rt = new Runtime(); + $block = $rt->parseAndCompile($code, 'issue_30645.php'); + ob_start(); + $rt->run($block); + $out = ob_get_clean(); + $this->assertStringContainsString( + 'ValueError: stream_context_set_option(): Argument #3 ($option_name) cannot be null when argument #2 ($wrapper_or_options) is a string', + $out + ); + $this->assertStringContainsString( + 'ValueError: stream_context_set_option(): Argument #4 ($value) must be provided when argument #2 ($wrapper_or_options) is a string', + $out + ); + $this->assertStringContainsString("true\ntrue\n", $out); + $this->assertStringNotContainsString('LogicException', $out); + $this->assertStringNotContainsString('NO_THROW', $out); + $this->assertStringNotContainsString('missing wrapper/option/value', $out); + } +}