Make WordPress Core


Ignore:
Timestamp:
10/16/2025 08:58:26 PM (12 months ago)
Author:
dmsnell
Message:

Charset: Rely on new UTF-8 pipeline for mb_strlen() fallback.

The existing polyfill for mb_strlen() contains a number of issues leaving plenty of opportunity for improvement. Specifically, the following are all deficiencies: it relies on Unicode PCRE support, assumes input strings are valid UTF-8, splits input strings into an array of character to count them (1,000 at a time, iterating until complete), and entirely gives up when the Unicode support is missing.

This patch provides an updated polyfill which will reliably count code points in a UTF-8 string, even in the presence of sequences of invalid bytes. It scans through the input with zero allocations. Additionally, the underlying fallback extends the behavior of mb_strlen() to provide character counts for substrings within a larger input without extracting the substring (it can counts characters within a byte offset and length of a larger string).

This change improves the reliability of UTF-8 string length calculations and removes behavioral variability based on the runtime system.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/9828
Discussed in https://core.trac.wordpress.org/ticket/63863

See #63863.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/compat/mbStrlen.php

    r55562 r60949  
    1111
    1212        /**
    13          * Test that mb_strlen() is always available (either from PHP or WP).
     13         * Test that the native mb_strlen() is available.
    1414         */
    1515        public function test_mb_strlen_availability() {
    16                 $this->assertTrue( function_exists( 'mb_strlen' ) );
     16                $this->assertTrue(
     17                        in_array( 'mb_strlen', get_defined_functions()['internal'], true ),
     18                        'Test runner should have `mbstring` extension active but doesn’t.'
     19                );
    1720        }
    1821
    1922        /**
    20          * @dataProvider data_utf8_string_lengths
     23         * @dataProvider data_utf8_strings
    2124         */
    22         public function test_mb_strlen( $input_string, $expected_character_length ) {
    23                 $this->assertSame( $expected_character_length, _mb_strlen( $input_string, 'UTF-8' ) );
     25        public function test_mb_strlen( $input_string ) {
     26                $this->assertSame(
     27                        mb_strlen( $input_string, 'UTF-8' ),
     28                        _mb_strlen( $input_string, 'UTF-8' )
     29                );
    2430        }
    2531
    2632        /**
    27          * @dataProvider data_utf8_string_lengths
     33         * @dataProvider data_utf8_strings
    2834         */
    29         public function test_mb_strlen_via_regex( $input_string, $expected_character_length ) {
    30                 _wp_can_use_pcre_u( false );
    31                 $this->assertSame( $expected_character_length, _mb_strlen( $input_string, 'UTF-8' ) );
    32                 _wp_can_use_pcre_u( 'reset' );
     35        public function test_mb_strlen_via_regex( $input_string ) {
     36                $this->assertSame(
     37                        mb_strlen( $input_string, 'UTF-8' ),
     38                        _mb_strlen( $input_string, 'UTF-8' )
     39                );
    3340        }
    3441
    3542        /**
    36          * @dataProvider data_utf8_string_lengths
     43         * @dataProvider data_utf8_strings
    3744         */
    38         public function test_8bit_mb_strlen( $input_string, $expected_character_length, $expected_byte_length ) {
    39                 $this->assertSame( $expected_byte_length, _mb_strlen( $input_string, '8bit' ) );
     45        public function test_8bit_mb_strlen( $input_string ) {
     46                $this->assertSame(
     47                        mb_strlen( $input_string, '8bit' ),
     48                        _mb_strlen( $input_string, '8bit' )
     49                );
    4050        }
    4151
    … …  
    4555         * @return array
    4656         */
    47         public function data_utf8_string_lengths() {
     57        public function data_utf8_strings() {
    4858                return array(
    49                         array(
    50                                 'input_string'              => 'баба',
    51                                 'expected_character_length' => 4,
    52                                 'expected_byte_length'      => 8,
    53                         ),
    54                         array(
    55                                 'input_string'              => 'баб',
    56                                 'expected_character_length' => 3,
    57                                 'expected_byte_length'      => 6,
    58                         ),
    59                         array(
    60                                 'input_string'              => 'I am your б',
    61                                 'expected_character_length' => 11,
    62                                 'expected_byte_length'      => 12,
    63                         ),
    64                         array(
    65                                 'input_string'              => '1111111111',
    66                                 'expected_character_length' => 10,
    67                                 'expected_byte_length'      => 10,
    68                         ),
    69                         array(
    70                                 'input_string'              => '²²²²²²²²²²',
    71                                 'expected_character_length' => 10,
    72                                 'expected_byte_length'      => 20,
    73                         ),
    74                         array(
    75                                 'input_string'              => '3333333333',
    76                                 'expected_character_length' => 10,
    77                                 'expected_byte_length'      => 30,
    78                         ),
    79                         array(
    80                                 'input_string'              => '𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜',
    81                                 'expected_character_length' => 10,
    82                                 'expected_byte_length'      => 40,
    83                         ),
    84                         array(
    85                                 'input_string'              => '1²3𝟜1²3𝟜1²3𝟜',
    86                                 'expected_character_length' => 12,
    87                                 'expected_byte_length'      => 30,
    88                         ),
     59                        array( 'баба' ),
     60                        array( 'баб' ),
     61                        array( 'I am your б' ),
     62                        array( '1111111111' ),
     63                        array( '²²²²²²²²²²' ),
     64                        array( '3333333333' ),
     65                        array( '𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜𝟜' ),
     66                        array( '1²3𝟜1²3𝟜1²3𝟜' ),
    8967                );
    9068        }
Note: See TracChangeset for help on using the changeset viewer.