From 230b3dc5c4ddd5424021ebf14f93bbd791d4e2e0 Mon Sep 17 00:00:00 2001 From: Mihail Buryakov Date: Wed, 4 Feb 2026 17:02:13 +0200 Subject: [PATCH] [ijent] IJPL-232220 extract windows path manipulation into separate class, add tests GitOrigin-RevId: e0c538696d5eff165109cf056640efbc2992db89 --- .../eel/provider/EelNioBridgeService.kt | 31 +++---- .../eel/provider/utils/WindowsPathUtils.kt | 81 +++++++++++++++++++ .../eel/path/ArrayListEelAbsolutePath.kt | 2 +- .../fs/IjentEphemeralRootAwareFileSystem.kt | 16 ++-- .../execution/eel/WindowsPathUtilsTest.kt | 65 +++++++++++++++ 5 files changed, 167 insertions(+), 28 deletions(-) create mode 100644 platform/eel-provider/src/com/intellij/platform/eel/provider/utils/WindowsPathUtils.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/execution/eel/WindowsPathUtilsTest.kt diff --git a/platform/eel-provider/src/com/intellij/platform/eel/provider/EelNioBridgeService.kt b/platform/eel-provider/src/com/intellij/platform/eel/provider/EelNioBridgeService.kt index 6bff00640ed5..ecdfc0a9c909 100644 --- a/platform/eel-provider/src/com/intellij/platform/eel/provider/EelNioBridgeService.kt +++ b/platform/eel-provider/src/com/intellij/platform/eel/provider/EelNioBridgeService.kt @@ -9,9 +9,9 @@ import com.intellij.platform.eel.EelDescriptor import com.intellij.platform.eel.EelOsFamily import com.intellij.platform.eel.EelPathBoundDescriptor import com.intellij.platform.eel.annotations.MultiRoutingFileSystemPath -import com.intellij.platform.eel.isPosix import com.intellij.platform.eel.path.EelPath import com.intellij.platform.eel.path.EelPathException +import com.intellij.platform.eel.provider.utils.WindowsPathUtils import org.jetbrains.annotations.ApiStatus import java.nio.file.Path import kotlin.io.path.pathString @@ -44,8 +44,6 @@ fun EelPath.asNioPath(project: Project?): @MultiRoutingFileSystemPath Path { return asNioPath() } -private val WINDOWS_DRIVE_PREFIX_REGEX = Regex("^\\w:") - /** See docs for [asNioPath] */ @Deprecated("It never returns null anymore") @ApiStatus.Experimental @@ -68,12 +66,7 @@ fun EelPath.asNioPathOrNull(): @MultiRoutingFileSystemPath Path? { @MultiRoutingFileSystemPath val result = when (descriptor.osFamily) { EelOsFamily.Windows -> { - if (WINDOWS_DRIVE_PREFIX_REGEX.containsMatchIn(this.root.toString())) { - (listOf("@", this.root.toString().take(1)) + parts).fold(root, Path::resolve) - } - else { - parts.fold(root, Path::resolve) - } + WindowsPathUtils.resolveEelPathOntoRoot(root, this) } EelOsFamily.Posix -> parts.fold(root, Path::resolve) } @@ -111,17 +104,17 @@ fun Path.asEelPath(): EelPath { fun Path.asEelPath(descriptor: EelDescriptor): EelPath { when (descriptor) { is LocalEelDescriptor -> return EelPath.parse(toString(), descriptor) - is EelPathBoundDescriptor if (descriptor.osFamily.isPosix) -> { - val root = descriptor.rootPath - val relative = root.relativize(this) - return relative.fold(EelPath.parse("/", descriptor)) { path, part -> - part.toString().takeIf { it.isNotEmpty() }?.let { path.getChild(it) } ?: path - } - } is EelPathBoundDescriptor -> { - val root = descriptor.rootPath - val relative = root.relativize(this) - return relative.drop(1).fold(EelPath.parse(relative.first().pathString, descriptor)) { path, part -> + val relative = descriptor.rootPath.relativize(this) + val (eelRoot, rest) = when (descriptor.osFamily) { + EelOsFamily.Posix -> { + "/" to relative + } + EelOsFamily.Windows -> { + WindowsPathUtils.rootRelativeToEelPath(relative) + } + } + return rest.fold(EelPath.parse(eelRoot, descriptor)) { path, part -> part.toString().takeIf { it.isNotEmpty() }?.let { path.getChild(it) } ?: path } } diff --git a/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/WindowsPathUtils.kt b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/WindowsPathUtils.kt new file mode 100644 index 000000000000..cf397829cb85 --- /dev/null +++ b/platform/eel-provider/src/com/intellij/platform/eel/provider/utils/WindowsPathUtils.kt @@ -0,0 +1,81 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.eel.provider.utils + +import com.intellij.platform.eel.path.EelPath +import org.jetbrains.annotations.ApiStatus +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.pathString + +@ApiStatus.Internal +object WindowsPathUtils { + const val DRIVE_PATH_PREPEND: String = "@" + val WINDOWS_DRIVE_PREFIX_REGEX: Regex = Regex("^\\w:") + + /** + * Converts Windows [eelPath] into nio path under virtual mount [root]. + * + * If [root] is posix path `/$some.ij/mount/`, then the result will be`: + * `C:` -> `/$some.ij/mount/@/C` + * `C:\` -> `/$some.ij/mount/@/C/` + * `C:\Users` -> `/$some.ij/mount/@/C/Users` + * `\\server.share\dir\` -> `/$some.ij/mount/server.share/dir/` + * + * If [root] is Windows path `\\some.ij\mount\`, then the result will be: + * `C:` -> `\\some.ij\mount\@\C` + * `C:\` -> `\\some.ij\mount\@\C\` + * `C:\Users` -> `\\some.ij\mount\@\C\Users` + * `\\server.share\dir\` -> `\\some.ij\mount\server.share\dir\` + */ + fun resolveEelPathOntoRoot(root: Path, eelPath: EelPath): Path { + val rootParts = if (WINDOWS_DRIVE_PREFIX_REGEX.containsMatchIn(eelPath.root.toString())) { + listOf(DRIVE_PATH_PREPEND, eelPath.root.toString().take(1)) + } + else if (eelPath.root.toString().startsWith("\\\\")) { + eelPath.root.toString().removePrefix("\\\\").removeSuffix("\\").split("\\", limit = 2) + } else { + error("Unsupported root: ${eelPath.root}") + } + return (rootParts + eelPath.parts).fold(root, Path::resolve) + } + + /** + * Performs backward conversion. + * + * `@/C/Users` -> `C:\Users` + * `server.share/dir/` -> `\\server.share\dir\` + * + */ + fun rootRelativeToEelPath(relativePath: String): String { + return if (relativePath.startsWith("@/")) { + "${relativePath[2]}:${relativePath.drop(3)}" + } + else { + "\\\\$relativePath" + } + } + + /** + * Performs backward conversion. + * + * `@/C/Users` -> `C:`, `Users` + * `server.share/dir/tmp` -> `\\server.share\dir`, `tmp` + * + */ + fun rootRelativeToEelPath(relativePath: Path): Pair { + val rest = when (relativePath.nameCount) { + in 0..1 -> error("windows relative path should contain at least : $relativePath") + 2 -> Path("") + else -> relativePath.subpath(2, relativePath.nameCount) + } + val root = if (relativePath.startsWith(Path("@"))) { + "${relativePath.elementAt(1).pathString}:" + } + else { + "\\\\${relativePath.elementAt(0).pathString}\\${relativePath.elementAt(1).pathString}" + + } + return root to rest + } + +} diff --git a/platform/eel/src/com/intellij/platform/eel/path/ArrayListEelAbsolutePath.kt b/platform/eel/src/com/intellij/platform/eel/path/ArrayListEelAbsolutePath.kt index 43d1964f7d01..8bf4f6a1c9a4 100644 --- a/platform/eel/src/com/intellij/platform/eel/path/ArrayListEelAbsolutePath.kt +++ b/platform/eel/src/com/intellij/platform/eel/path/ArrayListEelAbsolutePath.kt @@ -212,7 +212,7 @@ internal class ArrayListEelAbsolutePath private constructor( if (error != null) throw EelPathException(raw, error) } - return ArrayListEelAbsolutePath(descriptor, Root.Windows(raw.substring(0, index).replace("/", "\\")), parts) + return ArrayListEelAbsolutePath(descriptor, Root.Windows(raw.substring(0, index).replace("/", "\\") + "\\" + parts.first()), parts.drop(1)) } /** https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#traditional-dos-paths */ diff --git a/platform/ijent/impl/src/com/intellij/platform/ijent/community/impl/nio/fs/IjentEphemeralRootAwareFileSystem.kt b/platform/ijent/impl/src/com/intellij/platform/ijent/community/impl/nio/fs/IjentEphemeralRootAwareFileSystem.kt index 5194f5d1ee65..d326fd540c5a 100644 --- a/platform/ijent/impl/src/com/intellij/platform/ijent/community/impl/nio/fs/IjentEphemeralRootAwareFileSystem.kt +++ b/platform/ijent/impl/src/com/intellij/platform/ijent/community/impl/nio/fs/IjentEphemeralRootAwareFileSystem.kt @@ -12,6 +12,7 @@ import com.intellij.platform.eel.EelDescriptor import com.intellij.platform.eel.EelOsFamily import com.intellij.platform.eel.provider.getEelDescriptor import com.intellij.platform.eel.provider.utils.EelPathUtils + import com.intellij.platform.eel.provider.utils.WindowsPathUtils import com.intellij.platform.ijent.community.impl.nio.IjentNioPath import com.intellij.util.text.nullize import org.jetbrains.annotations.ApiStatus @@ -33,7 +34,6 @@ import java.nio.file.attribute.BasicFileAttributes import java.nio.file.attribute.FileAttributeView import java.nio.file.attribute.UserPrincipalLookupService import java.nio.file.spi.FileSystemProvider -import kotlin.io.path.Path import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.pathString @@ -401,14 +401,14 @@ class IjentEphemeralRootAwareFileSystem( } EelOsFamily.Windows -> { if (relativePath != null) { - if (relativePath.startsWith("/@/")) { - arrayOf("${relativePath[3]}:${relativePath.drop(4)}", *parts) - } - else { - arrayOf(relativePath.removePrefix("/"), *parts) - } + arrayOf(WindowsPathUtils.rootRelativeToEelPath(relativePath.removePrefix("/")), *parts) + } + else if (path.isNotEmpty()) { + parts + } + else { + error("Windows $path is not under any root of $root") } - else parts } } } diff --git a/platform/platform-tests/testSrc/com/intellij/execution/eel/WindowsPathUtilsTest.kt b/platform/platform-tests/testSrc/com/intellij/execution/eel/WindowsPathUtilsTest.kt new file mode 100644 index 000000000000..a44bb5cb5593 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/execution/eel/WindowsPathUtilsTest.kt @@ -0,0 +1,65 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.eel + +import com.intellij.platform.eel.EelDescriptor +import com.intellij.platform.eel.EelOsFamily +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.provider.utils.WindowsPathUtils +import com.intellij.util.system.OS +import org.jetbrains.annotations.NonNls +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import java.io.File +import kotlin.io.path.Path +import kotlin.io.path.pathString + +class WindowsPathUtilsTest { + + class TestEelDescriptor(override val osFamily: EelOsFamily) : EelDescriptor { + override val name: @NonNls String get() = "Test Descriptor" + } + + @ParameterizedTest + @CsvSource( + textBlock = """ + C:\Users\test, @\C\Users\test + C:\, @\C\ + C:, @\C + D:\Program Files, @\D\Program Files + \\server\share\dir, server\share\dir + \\server\share\dir\, server\share\dir\ + \\server\share\, server\share\ + C:\Users\test, @\C\Users\test + C:\, @\C\ + C:, @\C + \\server\share\dir, server\share\dir""" + ) + fun `test resolveEelPathOntoRoot`(eelPathString: String, expected: String) { + val rootPath = if (OS.CURRENT == OS.Windows) { + Path("\\\\virtual.ij\\mount@") + } + else { + Path($$"""/$virtual.ij/mount@""") + } + val eelPath = EelPath.parse(eelPathString, TestEelDescriptor(EelOsFamily.Windows)) + val result = WindowsPathUtils.resolveEelPathOntoRoot(rootPath, eelPath) + assertEquals(rootPath.resolve(expected.replace("\\", File.separator)), result) + } + + @ParameterizedTest + @CsvSource( + textBlock = """ + @/C, C:, '' + @/C/Users, C:, Users + @/D/Program Files, D:, Program Files + @/D/Program Files/JetBrains, D:, Program Files/JetBrains + server.share/dir, \\server.share\dir, '' + server.share/dir/tmp, \\server.share\dir, tmp + server.share/dir/tmp/nested/file, \\server.share\dir, tmp/nested/file""" + ) + fun `test rootRelativeToEelPath with Path parameter`(relativePath: String, expectedRoot: String, expectedSubpath: String) { + assertEquals("$expectedRoot${if (expectedSubpath.isEmpty()) "" else "\\$expectedSubpath"}".replace("\\", File.separator), WindowsPathUtils.rootRelativeToEelPath(relativePath).replace("\\", File.separator)) + assertEquals(expectedRoot to Path(expectedSubpath ?: ""), WindowsPathUtils.rootRelativeToEelPath(Path(relativePath))) + } +}