diff --git a/ext/standard/VmString.php b/ext/standard/VmString.php index b6d1655b685..ccb1fdb013d 100644 --- a/ext/standard/VmString.php +++ b/ext/standard/VmString.php @@ -1726,6 +1726,7 @@ public static function strspn(string $str, string $mask, int $offset = 0, ?int $ /** * PHP 8.4 (GH-12592): empty $mask returns full segment byte length (not NUL-terminated walk). + * PROFILE<8.4: empty $mask stops at the first embedded NUL like classic php-src / C strcspn (#27716). */ public static function strcspn(string $str, string $mask, int $offset = 0, ?int $length = null): int { @@ -1735,7 +1736,19 @@ public static function strcspn(string $str, string $mask, int $offset = 0, ?int return 0; } if ('' === $mask) { - return $len; + if (!self::strcspnEmptyMaskStopsAtNul()) { + return $len; + } + // PROFILE≤8.3 empty mask — stop at embedded NUL within the segment (#27716). + $count = 0; + for ($i = $start; $i < $start + $len; ++$i) { + if ("\0" === $str[$i]) { + break; + } + ++$count; + } + + return $count; } $mlen = self::byteLength($mask); $count = 0; @@ -1749,6 +1762,22 @@ public static function strcspn(string $str, string $mask, int $offset = 0, ?int return $count; } + /** + * Empty $characters + embedded NUL (#27716 / GH-12592). + * + * Explicit PROFILE<8.4 matches Zend 8.2 (stop at NUL). Unset PROFILE and PROFILE≥8.4 keep + * the binary-safe full-segment length from #7088 (`strcspn_empty_characters_84`). + */ + public static function strcspnEmptyMaskStopsAtNul(): bool + { + $raw = getenv('PHP_COMPILER_PROFILE'); + if (!\is_string($raw) || '' === trim($raw)) { + return false; + } + + return version_compare(CompilerVersion::languageProfileVersion(), '8.4.0', '<'); + } + public static function strpbrk(string $str, string $mask) { if ('' === $mask) { throw new \ValueError('strpbrk(): Argument #2 ($characters) must be a non-empty string'); diff --git a/lib/JIT/Builtin/StringStrspn.php b/lib/JIT/Builtin/StringStrspn.php index 8a9f5420879..45526cd8ec8 100644 --- a/lib/JIT/Builtin/StringStrspn.php +++ b/lib/JIT/Builtin/StringStrspn.php @@ -243,7 +243,50 @@ private static function emitExtendedBody(Context $context): void $context->builder->branchIf($maskEmptyCspn, $cspnEmptyMaskBb, $loopSetupBb); $context->builder->positionAtEnd($cspnEmptyMaskBb); - $context->builder->returnValue($segLen); + // Unset PROFILE / PROFILE≥8.4 (GH-12592, #7088): empty mask → full segment length. + // Explicit PROFILE<8.4 (#27716): stop at first embedded NUL like Zend 8.2. + if (!\PHPCompiler\ext\standard\VmString::strcspnEmptyMaskStopsAtNul()) { + $context->builder->returnValue($segLen); + } else { + $nulCountSlot = $context->builder->alloca($i64, 1, 'cspn_empty_nul_cnt'); + $nulIdxSlot = $context->builder->alloca($i64, 1, 'cspn_empty_nul_i'); + $context->builder->store($zero, $nulCountSlot); + $context->builder->store($start, $nulIdxSlot); + $nulEnd = $context->builder->add($start, $segLen); + $nulHead = $fn->appendBasicBlock('cspn_empty_nul_head'); + $nulBody = $fn->appendBasicBlock('cspn_empty_nul_body'); + $nulInc = $fn->appendBasicBlock('cspn_empty_nul_inc'); + $nulDone = $fn->appendBasicBlock('cspn_empty_nul_done'); + $context->builder->branch($nulHead); + + $context->builder->positionAtEnd($nulHead); + $nulI = $context->builder->load($nulIdxSlot); + $nulAtEnd = $context->builder->icmp(Builder::INT_SGE, $nulI, $nulEnd); + $context->builder->branchIf($nulAtEnd, $nulDone, $nulBody); + + $context->builder->positionAtEnd($nulBody); + $nulByte = $context->builder->load($context->builder->gep($strData, $nulI)); + $isNul = $context->builder->icmp( + Builder::INT_EQ, + $nulByte, + $i8->constInt(0, false) + ); + $context->builder->branchIf($isNul, $nulDone, $nulInc); + + $context->builder->positionAtEnd($nulInc); + $context->builder->store( + $context->builder->add($context->builder->load($nulCountSlot), $one), + $nulCountSlot + ); + $context->builder->store( + $context->builder->add($nulI, $one), + $nulIdxSlot + ); + $context->builder->branch($nulHead); + + $context->builder->positionAtEnd($nulDone); + $context->builder->returnValue($context->builder->load($nulCountSlot)); + } $context->builder->positionAtEnd($loopSetupBb); $iSlot = $context->builder->alloca($i64, 1); diff --git a/test/compliance/cases/stdlib/strcspn_empty_nul_profile82.phpt b/test/compliance/cases/stdlib/strcspn_empty_nul_profile82.phpt new file mode 100644 index 00000000000..f532a51b2ef --- /dev/null +++ b/test/compliance/cases/stdlib/strcspn_empty_nul_profile82.phpt @@ -0,0 +1,13 @@ +--TEST-- +stdlib strcspn() empty $characters + NUL — PROFILE=8.2 stops at NUL (#27716, GH-12592 inverse) +--ENV-- +PHP_COMPILER_PROFILE=8.2 +--FILE-- +