From ff6d2fc3539781249e129e5f7e108117f8fbf689 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 3 Jan 2026 15:08:53 -0500 Subject: [PATCH 1/2] refactor(Filesystem): use PathHelper for canonical normalization Signed-off-by: Josh --- lib/private/Files/Filesystem.php | 80 ++++++++++++++++---------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index eb374d9326b3d..913307d0da424 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -9,6 +9,7 @@ use OC\Files\Mount\MountPoint; use OC\Files\Storage\StorageFactory; +use OC\Files\Utils\PathHelper; use OC\User\NoUserException; use OCP\Cache\CappedMemoryCache; use OCP\EventDispatcher\IEventDispatcher; @@ -585,66 +586,67 @@ public static function hasUpdated($path, $time) { } /** - * Fix common problems with a file path + * Normalizes a file path for consistent use within the virtual filesystem. + * + * Applies Unicode normalization (optional), removes dot-segments and duplicate slashes, + * enforces leading slash, and optionally strips trailing slashes. Results are cached + * per unique input to improve performance. + * + * TODO: Better unify with Pathhelper::normalizePath() (and possibly others) + * TODO: Benchmark whether the cache is still beneficial today * - * @param string $path - * @param bool $stripTrailingSlash whether to strip the trailing slash - * @param bool $isAbsolutePath whether the given path is absolute - * @param bool $keepUnicode true to disable unicode normalization * @psalm-taint-escape file - * @return string - */ - public static function normalizePath($path, $stripTrailingSlash = true, $isAbsolutePath = false, $keepUnicode = false) { - /** - * FIXME: This is a workaround for existing classes and files which call - * this function with another type than a valid string. This - * conversion should get removed as soon as all existing - * function calls have been fixed. - */ - $path = (string)$path; - - if ($path === '') { + * + * @param string $path The file path to normalize. + * @param bool $stripTrailingSlash Remove the trailing slash (except root). Default: true. + * @param bool $isAbsolutePath Ignored; kept for legacy compatibility. + * @param bool $keepUnicode If true, skips Unicode normalization. Default: false. + * @return string Normalized path. + */ + public static function normalizePath( + string $path, + bool $stripTrailingSlash = true, + bool $isAbsolutePath = false, + bool $keepUnicode = false + ): string { + // Early return for root and empty string + if ($path === '' || $path === '/') { return '/'; } + // Prepare cache if (is_null(self::$normalizedPathCache)) { self::$normalizedPathCache = new CappedMemoryCache(2048); } - $cacheKey = json_encode([$path, $stripTrailingSlash, $isAbsolutePath, $keepUnicode]); - if ($cacheKey && isset(self::$normalizedPathCache[$cacheKey])) { return self::$normalizedPathCache[$cacheKey]; } - //normalize unicode if possible + // Unicode normalization if (!$keepUnicode) { $path = \OC_Util::normalizeUnicode($path); } - //add leading slash, if it is already there we strip it anyway - $path = '/' . $path; - - $patterns = [ - '#\\\\#s', // no windows style '\\' slashes - '#/\.(/\.)*/#s', // remove '/./' - '#\//+#s', // remove sequence of slashes - '#/\.$#s', // remove trailing '/.' - ]; + // Canonical normalization via PathHelper + $normalized = PathHelper::normalizePath($path); - do { - $count = 0; - $path = preg_replace($patterns, '/', $path, -1, $count); - } while ($count > 0); - - //remove trailing slash - if ($stripTrailingSlash && strlen($path) > 1) { - $path = rtrim($path, '/'); + // TEMPORARY: Remove dot-segments here until PathHelper::normalizePath() is assessed/updated to handle them natively + while (\str_contains($normalized, '/./')) { + $normalized = \str_replace('/./', '/', $normalized); + } + // Remove trailing '/.' (unless the whole thing is '/.') + if (\substr($normalized, -2) === '/.' && \strlen($normalized) > 2) { + $normalized = \substr($normalized, 0, -2); } - self::$normalizedPathCache[$cacheKey] = $path; + // Optionally strip trailing slash unless root + if ($stripTrailingSlash && \strlen($normalized) > 1) { + $normalized = \rtrim($normalized, '/'); + } - return $path; + self::$normalizedPathCache[$cacheKey] = $normalized; + return $normalized; } /** From 0de4631714d581736b2f26a8c580e9a080b26f15 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 10 Jan 2026 12:13:46 -0500 Subject: [PATCH 2/2] chore: normalize trailing slash handling Signed-off-by: Josh --- lib/private/Files/Filesystem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/private/Files/Filesystem.php b/lib/private/Files/Filesystem.php index 913307d0da424..66693405b6d09 100644 --- a/lib/private/Files/Filesystem.php +++ b/lib/private/Files/Filesystem.php @@ -645,6 +645,13 @@ public static function normalizePath( $normalized = \rtrim($normalized, '/'); } + // Add a trailing slash only if requested (and not root), and ONLY if not already present + if ($normalized === '') { + $normalized = '/'; + } elseif (!$stripTrailingSlash && substr($normalized, -1) !== '/') { + $normalized .= '/'; + } + self::$normalizedPathCache[$cacheKey] = $normalized; return $normalized; }