Skip to content

Commit 75990a5

Browse files
IJPL-213371: support resolve for symlinks that point to files with special chars on WSL.
See: `com.intellij.platform.ijent.functional.IjentSpecialCharsTest.testCharInSymLink` Merge-request: IJ-MR-214143 Merged-by: Ilya Kazakevich <ilya.kazakevich@jetbrains.com> GitOrigin-RevId: 3d51da52581b5b8abbd9357e2151a2a52570df2d
1 parent 581c329 commit 75990a5

3 files changed

Lines changed: 110 additions & 19 deletions

File tree

platform/eel/src/com/intellij/platform/eel/path/EelPath.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ val EelPath.platform: EelOsFamily get() = descriptor.osFamily
1919
* All operations listed here do not require I/O.
2020
*
2121
* In the examples below, `descriptor` is a POSIX [EelDescriptor] and `windowsDescriptor` is a Windows one.
22+
*
23+
* [EelPath] contains both: [EelDescriptor] and a path on it (e.g. a descriptor of a Docker container and path `/root` inside of it).
24+
* To get the path on [EelDescriptor] (to pass it to a remote command for example), use [toString];
25+
* [fileName] returns only the last component of it.
2226
*/
2327
@ApiStatus.Experimental
2428
sealed interface EelPath {

platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ijent/nio/IjentWslNioFileSystemProvider.kt

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ package com.intellij.platform.ide.impl.wsl.ijent.nio
44
import com.intellij.execution.wsl.WslPath
55
import com.intellij.openapi.diagnostic.logger
66
import com.intellij.openapi.util.NlsSafe
7+
import com.intellij.platform.core.nio.fs.MultiRoutingFsPath
78
import com.intellij.platform.core.nio.fs.RoutingAwareFileSystemProvider
89
import com.intellij.platform.eel.EelDescriptor
910
import com.intellij.platform.eel.provider.utils.EelPathTransfer
11+
import com.intellij.platform.eel.provider.utils.impl.ijentToLocal
1012
import com.intellij.platform.eel.provider.utils.impl.localToIjent
1113
import com.intellij.platform.ide.impl.wsl.WSL_PREFIXES
1214
import com.intellij.platform.ijent.community.impl.nio.IjentNioPath
@@ -93,7 +95,10 @@ internal class IjentWslNioFileSystemProvider(
9395
val notationLowerCase = notation.lowercase()
9496
assert(notationLowerCase in WSL_PREFIXES) { notation }
9597
return when (this) {
96-
is IjentNioPath -> fold(originalFs.getPath("\\\\$notation\\$wslId\\")) { parent, file -> parent.resolve(file.toString()) }
98+
// `ijentToLocal` is mandatory: chars like `:` are legal in Linux file names, but `WindowsPath` can't hold them as is.
99+
is IjentNioPath -> fold(originalFs.getPath("\\\\$notation\\$wslId\\")) { parent, file ->
100+
parent.resolve(ijentToLocal(file.toString()))
101+
}
97102
is IjentWslNioPath -> presentablePath.toOriginalPath(notation)
98103
else -> this
99104
}
@@ -103,6 +108,8 @@ internal class IjentWslNioFileSystemProvider(
103108
return when (this) {
104109
is IjentNioPath -> error(this)
105110
is IjentWslNioPath -> presentablePath.toOriginalPathWithSameNotation()
111+
// The original file system provider throws ProviderMismatchException for paths of the routing file system.
112+
is MultiRoutingFsPath -> initialDelegate.toOriginalPathWithSameNotation()
106113
else -> this
107114
}
108115
}
@@ -132,14 +139,25 @@ internal class IjentWslNioFileSystemProvider(
132139
}
133140
}
134141

135-
private fun wslIdFromPath(path: Path): String? {
136-
val root = path.toAbsolutePath().root.toString()
142+
private fun wslIdFromPath(path: Path): String? = wslIdFromRoot(path.toAbsolutePath().root.toString())
143+
144+
private fun wslIdFromRoot(root: String): String? {
137145
val wslMarker = """\\wsl"""
138146
if (!root.startsWith(wslMarker, ignoreCase = true)) return null
139147
val wslId = root.substring(wslMarker.length).substringAfter('\\').trimEnd('\\')
140148
return wslId.ifEmpty { null }
141149
}
142150

151+
/**
152+
* `wsl$` or `wsl.localhost`, taken from [root] which looks like `\\wsl$\distro\`.
153+
* A relative path has no root to take the notation from, so an empty [root] is accepted and yields the fallback.
154+
*/
155+
internal fun notationFromRoot(root: String): String {
156+
val notation = root.removePrefix("""\\""").substringBefore('\\')
157+
// `wsl.localhost` is the notation supported by all WSL versions, hence it is the fallback.
158+
return if (WSL_NOTATIONS.any { it.equals(notation, ignoreCase = true) }) notation else WSL_NOTATIONS[0]
159+
}
160+
143161
override fun checkAccess(path: Path, vararg modes: AccessMode): Unit = ijentFsProvider.checkAccess(path.toIjentPath(), *modes)
144162

145163
override fun newInputStream(path: Path, vararg options: OpenOption?): InputStream =
@@ -171,12 +189,24 @@ internal class IjentWslNioFileSystemProvider(
171189

172190
override fun deleteIfExists(path: Path): Boolean = ijentFsProvider.deleteIfExists(path.toIjentPath())
173191

174-
override fun readSymbolicLink(link: Path): IjentWslNioPath = IjentWslNioPath(
175-
getFileSystem(wslIdFromPath(link)
176-
?: throw IOException("Cannot find WSL distribution for $link. The URL is incorrect or the distribution does not exist.")),
177-
ijentFsProvider.readSymbolicLink(link.toIjentPath()),
178-
null,
179-
)
192+
override fun readSymbolicLink(link: Path): IjentWslNioPath {
193+
val root = link.toAbsolutePath().root.toString()
194+
val wslId = wslIdFromRoot(root)
195+
?: throw IOException("Cannot find WSL distribution for $link. The URL is incorrect or the distribution does not exist.")
196+
val target = ijentFsProvider.readSymbolicLink(link.toIjentPath())
197+
// The link target is what the user and the tools see, so it must be a Windows path even though IJent reports a Linux one:
198+
// otherwise `link.parent.resolve(target)` and `target.toString()` produce garbage for targets with chars like `:` in them.
199+
val presentableTarget =
200+
if (target.isAbsolute) {
201+
// The notation is taken from `link` because `\\wsl$\` and `\\wsl.localhost\` must never be mixed within one path.
202+
target.toOriginalPath(notationFromRoot(root))
203+
}
204+
else {
205+
// A relative target has no root, hence no notation; only the special chars have to be mapped.
206+
originalFs.getPath(ijentToLocal(target.toString()))
207+
}
208+
return IjentWslNioPath(getFileSystem(wslId), presentableTarget, null)
209+
}
180210

181211
override fun getPath(uri: URI): Path = IjentWslNioPath(
182212
getFileSystem(wslIdFromPath(originalFsProvider.getPath(uri))
@@ -317,5 +347,11 @@ internal class IjentWslNioFileSystemProvider(
317347

318348
companion object {
319349
private val LOG = logger<IjentWslNioFileSystemProvider>()
350+
351+
/**
352+
* The two interchangeable notations of a WSL UNC root.
353+
* They must never be mixed within one path, see [com.intellij.platform.eel.provider.asNioPath].
354+
*/
355+
internal val WSL_NOTATIONS: List<String> = listOf("wsl.localhost", "wsl$")
320356
}
321357
}

platform/platform-impl/eel/src/com/intellij/platform/ide/impl/wsl/ijent/nio/IjentWslNioPath.kt

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
package com.intellij.platform.ide.impl.wsl.ijent.nio
33

44
import com.intellij.platform.core.nio.fs.BasicFileAttributesHolder2
5+
import com.intellij.platform.core.nio.fs.MultiRoutingFsPath
56
import com.intellij.platform.eel.provider.utils.EelPathUtils.getActualPath
7+
import com.intellij.platform.eel.provider.utils.impl.ijentToLocal
8+
import com.intellij.platform.eel.provider.utils.impl.localToIjent
69
import com.intellij.platform.ide.impl.wsl.WSL_PREFIXES
10+
import com.intellij.platform.ijent.community.impl.nio.IjentNioPath
711
import com.intellij.platform.ijent.community.impl.nio.fs.IjentNioPosixFileAttributesWithDosAdapter
812
import java.net.URI
913
import java.nio.file.LinkOption
1014
import java.nio.file.Path
1115
import java.nio.file.WatchEvent
1216
import java.nio.file.WatchKey
1317
import java.nio.file.WatchService
18+
import kotlin.io.path.pathString
1419

1520
internal class IjentWslNioPath(
1621
private val fileSystem: IjentWslNioFileSystem,
@@ -23,7 +28,12 @@ internal class IjentWslNioPath(
2328
cachedAttributes: IjentNioPosixFileAttributesWithDosAdapter?,
2429
) : Path, BasicFileAttributesHolder2.Impl(cachedAttributes) {
2530
init {
26-
require(presentablePath !is IjentWslNioPath) { "IjentWslNioPath should be a wrapper over other instances of path, namely WindowsPath or IjentNioPath" }
31+
// `MultiRoutingFsPath` is rejected as well: it may delegate to an `IjentWslNioPath`, and such nesting silently breaks
32+
// `equals`, `hashCode` and every call that passes `presentablePath` to the original (Windows) file system provider.
33+
require(presentablePath !is IjentWslNioPath && presentablePath !is MultiRoutingFsPath) {
34+
"IjentWslNioPath should be a wrapper over other instances of path, namely WindowsPath or IjentNioPath," +
35+
" but got ${presentablePath.javaClass.name}: $presentablePath"
36+
}
2737
}
2838

2939
val actualPath: Path = getActualPath(presentablePath)
@@ -50,13 +60,18 @@ internal class IjentWslNioPath(
5060

5161
override fun normalize(): IjentWslNioPath = presentablePath.normalize().toIjentWslPath()
5262

53-
override fun resolve(other: Path): IjentWslNioPath = presentablePath.resolve(other.toOriginalPath()).toIjentWslPath()
63+
override fun resolve(other: Path): IjentWslNioPath {
64+
val otherPath = other.toSameFlavourAsPresentablePath()
65+
// `Path.resolve` returns `other` as is when it is absolute, but the result still has to be a path of this file system.
66+
return if (otherPath.isAbsolute) otherPath.toIjentWslPath()
67+
else presentablePath.resolve(otherPath).toIjentWslPath()
68+
}
5469

5570
override fun relativize(other: Path): IjentWslNioPath {
5671
if (isAbsolute != other.isAbsolute) {
5772
throw IllegalArgumentException("Tried to relativize a relative and an absolute path: `$this` and `$other`." + " Check for possible confusion." + " Maybe some code up the call stack tried to use a path from the Linux machine as a WSL path for Windows.")
5873
}
59-
return presentablePath.relativize(other.toOriginalPath()).toIjentWslPath()
74+
return presentablePath.relativize(other.toSameFlavourAsPresentablePath()).toIjentWslPath()
6075
}
6176

6277
override fun toUri(): URI = presentablePath.toUri()
@@ -83,10 +98,7 @@ internal class IjentWslNioPath(
8398
else {
8499
ijentNioPath.toRealPath(*options)
85100
}
86-
val originalPath = fileSystem.provider().toOriginalPath(
87-
path = ijentNioRealPath,
88-
notation = root.toString().removePrefix("\\\\").substringBefore('\\'),
89-
)
101+
val originalPath = fileSystem.provider().toOriginalPath(path = ijentNioRealPath, notation = presentableNotation)
90102
return originalPath.toIjentWslPath()
91103
}
92104

@@ -97,10 +109,49 @@ internal class IjentWslNioPath(
97109

98110
override fun compareTo(other: Path): Int = presentablePath.compareTo(other.toOriginalPath())
99111

100-
private fun Path.toIjentWslPath(): IjentWslNioPath = IjentWslNioPath(this@IjentWslNioPath.fileSystem, this, null)
112+
private fun Path.toIjentWslPath(): IjentWslNioPath =
113+
this as? IjentWslNioPath ?: IjentWslNioPath(this@IjentWslNioPath.fileSystem, this, null)
114+
115+
private fun Path.toOriginalPath(): Path = when (this) {
116+
is IjentWslNioPath -> this.presentablePath.toOriginalPath()
117+
// A path of the routing file system may delegate to a path of this very file system, so it is not a foreign path.
118+
is MultiRoutingFsPath -> this.initialDelegate.toOriginalPath()
119+
else -> this
120+
}
101121

102-
private fun Path.toOriginalPath(): Path = if (this is IjentWslNioPath) this.presentablePath
103-
else this
122+
/**
123+
* Returns [this] converted to the same kind of path as [presentablePath] (i.e. `WindowsPath` or [IjentNioPath]),
124+
* so that both can be used together in a single [Path] operation.
125+
* Special chars like `:` are mapped in the direction that [presentablePath] requires, see [ijentToLocal] and [localToIjent].
126+
*
127+
* This is the same trick as [MultiRoutingFsPath.toSameTypeAsDelegate]:
128+
* it is always the *argument* that is brought to the flavour of the receiver, never the other way round.
129+
*/
130+
private fun Path.toSameFlavourAsPresentablePath(): Path {
131+
val originalPath = toOriginalPath()
132+
return when {
133+
presentablePath.javaClass == originalPath.javaClass -> originalPath
134+
// An absolute IJent path has no `\\wsl$\distro\` prefix, and that prefix must use the same notation as this path.
135+
originalPath is IjentNioPath && originalPath.isAbsolute ->
136+
this@IjentWslNioPath.fileSystem.provider().toOriginalPath(originalPath, presentableNotation)
137+
138+
originalPath is IjentNioPath ->
139+
presentablePath.fileSystem.getPath(ijentToLocal(originalPath.pathString))
140+
141+
presentablePath is IjentNioPath ->
142+
presentablePath.fileSystem.getPath(localToIjent(originalPath.pathString.replace('\\', '/')))
143+
144+
else ->
145+
presentablePath.fileSystem.getPath(originalPath.pathString)
146+
}
147+
}
148+
149+
/**
150+
* `wsl$` or `wsl.localhost`: the notation used by this path.
151+
* These two must never be mixed within one path, see [com.intellij.platform.eel.provider.asNioPath].
152+
*/
153+
private val presentableNotation: String
154+
get() = fileSystem.provider().notationFromRoot(presentablePath.root?.toString() ?: "")
104155

105156
override fun toString(): String = presentablePath.toString()
106157

0 commit comments

Comments
 (0)