Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
31 changes: 29 additions & 2 deletions src/wp-includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2184,7 +2184,24 @@ function path_join( $base, $path ) {
* @return string Normalized path.
*/
function wp_normalize_path( $path ) {
Comment thread
josephscott marked this conversation as resolved.
Outdated
$wrapper = '';
$path = (string) $path;

static $hot = array();
Comment thread
josephscott marked this conversation as resolved.
Outdated
static $warm = array();
static $max = 100;

if ( isset( $hot[ $path ] ) ) {
return $hot[ $path ];
}

if ( isset( $warm[ $path ] ) ) {
$hot[ $path ] = $warm[ $path ];
Comment thread
josephscott marked this conversation as resolved.
Outdated
unset( $warm[ $path ] );
return $hot[ $path ];
}

$original_path = $path;
$wrapper = '';

if ( wp_is_stream( $path ) ) {
list( $wrapper, $path ) = explode( '://', $path, 2 );
Expand All @@ -2203,7 +2220,17 @@ function wp_normalize_path( $path ) {
$path = ucfirst( $path );
}

return $wrapper . $path;
$value = $wrapper . $path;

$hot[ $original_path ] = $value;

// Rotate segments when hot is full.
if ( count( $hot ) >= $max ) {
$warm = $hot;
$hot = array();
}

return $value;
}

/**
Expand Down
139 changes: 139 additions & 0 deletions tests/phpunit/tests/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,145 @@
array( 'php://input', 'php://input' ),
array( 'http://example.com//path.ext', 'http://example.com/path.ext' ),
array( 'file://c:\\www\\path\\', 'file://C:/www/path/' ),

// Edge cases.
array( '', '' ), // Empty string should return empty string.
array( 123, '123' ), // Integer should be cast to string.
);
}

/**
* Tests that wp_normalize_path() works with objects that have __toString().
*
* This is important because the function uses a static cache, and the input
* must be cast to string before being used as an array key.
*
* @ticket 64538
*/
public function test_wp_normalize_path_with_stringable_object() {
$stringable = new class() {
public function __toString() {
return '/var/www/html\\test';
}
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a more representative test might be an SplFileInfo instance, since that is actually used in Core. as discovered in #10781 we can fix that currently-improper call in another patch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no particular preference there. I will update the test_wp_normalize_path_with_stringable_object() to try that out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.


$this->assertSame( '/var/www/html/test', wp_normalize_path( $stringable ) );
}

/**
* Tests that wp_normalize_path() returns consistent results on repeated calls.
*
* The function uses a static cache, so this verifies cache behavior.
*
* @ticket 64538
*/
public function test_wp_normalize_path_returns_consistent_results() {
$path = 'C:\\www\\path\\';

$first_call = wp_normalize_path( $path );
$second_call = wp_normalize_path( $path );
$third_call = wp_normalize_path( $path );

$this->assertSame( $first_call, $second_call, 'Second call should return same result as first.' );
$this->assertSame( $second_call, $third_call, 'Third call should return same result as second.' );
$this->assertSame( 'C:/www/path/', $first_call, 'Normalized path should match expected value.' );
}
Comment thread
josephscott marked this conversation as resolved.

/**
* Tests that wp_normalize_path() cache rotation works correctly.
*
* The function uses a two-tier cache (hot/warm) with max 100 entries in hot.
* This verifies that after exceeding the cache size, both old and new paths
* still return correct results.
*
* @ticket 64538
*/
public function test_wp_normalize_path_cache_rotation() {
$paths = array();
$expected = array();

// Generate 150 unique paths to exceed the 100-entry hot cache limit.
for ( $i = 0; $i < 150; $i++ ) {
Comment thread
josephscott marked this conversation as resolved.
Outdated
$paths[ $i ] = "/var/www/test-{$i}\\subdir\\";
$expected[ $i ] = "/var/www/test-{$i}/subdir/";
}

// First pass: normalize all paths (fills hot, triggers rotation to warm).
$results_first_pass = array();
foreach ( $paths as $i => $path ) {
$results_first_pass[ $i ] = wp_normalize_path( $path );
}

// Verify all first pass results are correct.
foreach ( $results_first_pass as $i => $result ) {
$this->assertSame(
$expected[ $i ],
$result,
"First pass: Path {$i} should be normalized correctly."
);
}

// Second pass: verify old paths (now in warm) still return correct results.
// Access early paths which should have rotated to warm cache.
for ( $i = 0; $i < 50; $i++ ) {
$this->assertSame(
$expected[ $i ],
wp_normalize_path( $paths[ $i ] ),
"Second pass: Early path {$i} should still return correct result from warm cache."
);
}

// Verify recent paths (should be in hot cache) also work.
for ( $i = 100; $i < 150; $i++ ) {
$this->assertSame(
$expected[ $i ],
wp_normalize_path( $paths[ $i ] ),
"Second pass: Recent path {$i} should return correct result from hot cache."
);
}
Comment thread
josephscott marked this conversation as resolved.
Outdated
}

/**
* Tests that wp_normalize_path() cache segments do not exceed the maximum size.
*
* The function uses a two-tier cache with a max of 100 entries per segment.
* This verifies the cache remains bounded after processing many unique paths.
*
* @ticket 64538
*/
public function test_wp_normalize_path_cache_segment_limits() {
// Generate 250 unique paths to ensure multiple cache rotations.
for ( $i = 0; $i < 250; $i++ ) {
wp_normalize_path( "/unique/path/number-{$i}\\file.php" );
}

// Use reflection to inspect the static cache variables.
$reflection = new ReflectionFunction( 'wp_normalize_path' );

Check warning on line 338 in tests/phpunit/tests/functions.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Equals sign not aligned with surrounding assignments; expected 2 spaces but found 5 spaces
$static_vars = $reflection->getStaticVariables();

Check warning on line 339 in tests/phpunit/tests/functions.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Equals sign not aligned with surrounding assignments; expected 1 space but found 4 spaces
$hot_count = count( $static_vars['hot'] );

Check warning on line 340 in tests/phpunit/tests/functions.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Equals sign not aligned with surrounding assignments; expected 3 spaces but found 6 spaces
$warm_count = count( $static_vars['warm'] );

Check warning on line 341 in tests/phpunit/tests/functions.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Equals sign not aligned with surrounding assignments; expected 2 spaces but found 5 spaces
$max = $static_vars['max'];

Check warning on line 342 in tests/phpunit/tests/functions.php

View workflow job for this annotation

GitHub Actions / Coding standards / PHP checks

Equals sign not aligned with surrounding assignments; expected 9 spaces but found 12 spaces

// Verify hot cache does not exceed max.
$this->assertLessThanOrEqual(
$max,
$hot_count,
"Hot cache ({$hot_count} entries) should not exceed max ({$max})."
);

// Verify warm cache does not exceed max.
$this->assertLessThanOrEqual(
$max,
$warm_count,
"Warm cache ({$warm_count} entries) should not exceed max ({$max})."
);

// Verify total cached entries is bounded (at most 2x max).
$total = $hot_count + $warm_count;
$this->assertLessThanOrEqual(
$max * 2,
$total,
"Total cache ({$total} entries) should not exceed 2x max (" . ( $max * 2 ) . ').'
);
}

Expand Down
Loading