Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextEdit
import org.eclipse.lsp4j.WorkspaceEdit
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorPosition
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorRange
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorState
import java.io.File
import java.net.URI
import java.net.URISyntaxException

object LspEditorUtil {

Expand All @@ -45,25 +43,7 @@ object LspEditorUtil {
}

private fun toUri(file: File): URI {
try {
// URI scheme specified by language server protocol
val uri = URI("file", "", file.absoluteFile.toURI().path, null)
val fallback = file.toPath().toAbsolutePath().normalize().toUri()
return if (uri.isCompliant()) uri else fallback
} catch (e: URISyntaxException) {
LOG.warn { "${e.localizedMessage}: $e" }
return file.absoluteFile.toURI()
}
}

private fun URI.isCompliant(): Boolean {
if (!"file".equals(this.scheme, ignoreCase = true)) return true

val path = this.rawPath ?: this.path.orEmpty()
val noAuthority = this.authority.isNullOrEmpty()

// If the authority component is empty, the path cannot begin with two slash characters ("//")
return !(noAuthority && path.startsWith("//"))
return file.toPath().toAbsolutePath().normalize().toUri()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,322 @@ class FileUriUtilTest {
val result = LspEditorUtil.toUriString(virtualFile)
assertThat(result).isNull()
}

@Test
fun `test URI generation does not create problematic Windows patterns`() {
// This test ensures that URIs generated by toUriString() don't contain
// patterns that would cause issues when embedded in filesystem paths on Windows
val virtualFile = createMockVirtualFile("/path/to/test/file.txt")
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:")

// Critical: When this URI string is used as part of a filesystem path,
// it should not create mixed separators like "C:\Program Files\file:\C:\..."
// The fix ensures we use Path.toUri() which generates proper URIs
val uriString = result!!

// Verify it follows proper file URI format
assertThat(uriString).matches("file:///.*")

// Verify it doesn't contain Windows-style backslashes mixed with forward slashes
// (the original bug would create URIs that when stringified could cause this)
if (uriString.contains("\\")) {
// If backslashes are present, they should be properly encoded, not raw
assertThat(uriString).doesNotMatch(".*file:.*\\\\.*")
}
}

// ========== EXPANDED WINDOWS URI CONSTRUCTION BUG REGRESSION TESTS ==========

@Test
@EnabledOnOs(OS.WINDOWS)
fun `test Windows drive letters are handled correctly`() {
// Test various Windows drive letter scenarios
val testPaths = listOf(
"C:/Users/user/project/file.kt",
"D:/Development/aws-toolkit/test.java",
"E:/temp/amazonq/agents/config.json"
)

testPaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")
assertThat(result).doesNotContain("file:\\")
assertThat(result).doesNotContain("file:/C:")

// Should properly encode drive letter
assertThat(result).matches("file:///[A-Z]:/.*")
}
}

@Test
fun `test URI string cannot be embedded in filesystem paths`() {
// This is the core regression test for the Windows bug
val virtualFile = createMockVirtualFile("/Users/user/project/.amazonq/agents")
val uriString = LspEditorUtil.toUriString(virtualFile)

assertThat(uriString).isNotNull()

// Simulate how the URI would be used in a Windows filesystem path construction
val simulatedWindowsPath = "C:\\Program Files\\JetBrains\\PyCharm\\$uriString"

// The bug would create paths like: C:\Program Files\JetBrains\PyCharm\file:\C:\Users\...
// Verify this doesn't happen
assertThat(simulatedWindowsPath).doesNotContain("file:\\C:")
assertThat(simulatedWindowsPath).doesNotContain("file:/C:")

// Should be a valid URI format
assertThat(uriString).matches("file:///.*")
}

@Test
fun `test URI consistency across multiple calls`() {
// Ensure the URI generation is deterministic
val virtualFile = createMockVirtualFile("/project/src/main/kotlin/Test.kt")

val results = (1..10).map { LspEditorUtil.toUriString(virtualFile) }

// All results should be identical
assertThat(results.toSet()).hasSize(1)
assertThat(results.first()).isNotNull()
assertThat(results.first()).startsWith("file:///")
}

@Test
fun `test URI generation with special characters`() {
val specialPaths = listOf(
"/path/with spaces/file.txt",
"/path/with-dashes/file.txt",
"/path/with_underscores/file.txt",
"/path/with.dots/file.txt",
"/path/with(parentheses)/file.txt",
"/path/with[brackets]/file.txt",
"/path/with{braces}/file.txt",
"/path/with@symbols/file.txt",
"/path/with#hash/file.txt",
"/path/with%percent/file.txt"
)

specialPaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")

// Special characters should be properly encoded
if (path.contains(" ")) {
assertThat(result).contains("%20")
}
if (path.contains("(")) {
assertThat(result).contains("%28")
}
if (path.contains(")")) {
assertThat(result).contains("%29")
}
}
}

@Test
fun `test URI generation with unicode characters`() {
val unicodePaths = listOf(
"/path/with/café/file.txt",
"/path/with/文件/test.txt",
"/path/with/файл/document.txt",
"/path/with/ファイル/code.kt"
)

unicodePaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")

// Unicode should be properly encoded
assertThat(result).doesNotContain("\\u")
}
}

@Test
fun `test deeply nested paths`() {
val deepPath = "/very/deeply/nested/path/structure/that/goes/many/levels/deep/to/test/normalization/file.txt"
val virtualFile = createMockVirtualFile(deepPath)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")
assertThat(result).endsWith("/file.txt")

// Should not contain double slashes (except after scheme)
assertThat(result.substring(7)).doesNotContain("//") // Skip "file://"
}

@Test
fun `test path normalization edge cases`() {
val pathsToNormalize = listOf(
"/path/../to/file.txt", // Parent directory reference
"/path/./to/file.txt", // Current directory reference
"/path//to///file.txt", // Multiple slashes
"/path/to/../from/file.txt", // Mixed references
"/../../../root/file.txt" // Multiple parent references
)

pathsToNormalize.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")

// Normalized paths should not contain . or .. segments
assertThat(result).doesNotContain("/..")
assertThat(result).doesNotContain("/./")

// Should not contain multiple consecutive slashes
assertThat(result.substring(7)).doesNotContain("//")
}
}

@Test
fun `test case sensitivity preservation`() {
val caseSensitivePaths = listOf(
"/Path/To/File.TXT",
"/PATH/TO/FILE.txt",
"/path/to/file.TXT",
"/MyProject/SRC/Main/Kotlin/Test.kt"
)

caseSensitivePaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")

// Case should be preserved in the URI
val resultPath = result!!.substringAfter("file:///")
assertThat(resultPath).isNotEqualTo(resultPath.lowercase())
}
}

@Test
fun `test concurrent URI generation`() {
// Test thread safety of URI generation
val virtualFile = createMockVirtualFile("/concurrent/test/file.kt")
val results = mutableListOf<String?>()

// Simulate concurrent access
repeat(100) {
results.add(LspEditorUtil.toUriString(virtualFile))
}

// All results should be identical and valid
assertThat(results.toSet()).hasSize(1)
assertThat(results.first()).isNotNull()
assertThat(results.first()).startsWith("file:///")
}

@Test
@EnabledOnOs(OS.WINDOWS)
fun `test Windows MCP workspace configuration paths`() {
// Specific test for the MCP configuration bug scenario
val mcpPaths = listOf(
"C:/Users/user/project/.amazonq/agents",
"C:/Users/user/workspace/.amazonq/config",
"D:/Development/project/.amazonq/mcp/settings.json",
"E:/Projects/aws-toolkit/.amazonq/agents/workspace.json"
)

mcpPaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")

// Critical: When combined with IDE paths, should not create malformed paths
val ideBasePath = "C:\\Program Files\\JetBrains\\PyCharm"
val combinedPath = "$ideBasePath\\$result"

// Should not contain the problematic pattern
assertThat(combinedPath).doesNotContain("\\file:\\C:")
assertThat(combinedPath).doesNotContain("\\file:/C:")

// Verify proper URI format
assertThat(result).matches("file:///[A-Z]:/.*amazonq.*")
}
}

@Test
fun `test integration with actual File objects`() {
// Integration test using real File objects
val tempFile = kotlin.io.path.createTempFile("uri-test", ".tmp").toFile()
try {
val virtualFile = createMockVirtualFile(tempFile.absolutePath)
val result = LspEditorUtil.toUriString(virtualFile)

assertThat(result).isNotNull()
assertThat(result).startsWith("file:///")
assertThat(result).endsWith(".tmp")

// Should be a valid URI that can be parsed
val uri = java.net.URI.create(result!!)
assertThat(uri.scheme).isEqualTo("file")
assertThat(uri.path).isNotNull()
} finally {
tempFile.delete()
}
}

@Test
fun `test error recovery with malformed paths`() {
// Test how the system handles potentially malformed input paths
val malformedPaths = listOf(
"", // Empty path
" ", // Whitespace only
"/", // Root only
"relative/path", // Relative path (no leading slash)
"\\windows\\style", // Windows-style separators on Unix
"C:", // Drive letter only
"file://already/uri" // Already a URI
)

malformedPaths.forEach { path ->
val virtualFile = createMockVirtualFile(path)
val result = LspEditorUtil.toUriString(virtualFile)

if (result != null) {
// If a result is returned, it should be a valid URI
assertThat(result).startsWith("file:")

// Should not crash when parsed as URI
assertThat { java.net.URI.create(result) }.doesNotThrowAnyException()
}
}
}

@Test
fun `test performance with large number of paths`() {
// Performance regression test
val paths = (1..1000).map { "/performance/test/path/number/$it/file.kt" }

val startTime = System.currentTimeMillis()
val results = paths.map { path ->
val virtualFile = createMockVirtualFile(path)
LspEditorUtil.toUriString(virtualFile)
}
val endTime = System.currentTimeMillis()

// All results should be valid
assertThat(results).allMatch { it != null && it.startsWith("file:///") }

// Should complete reasonably quickly (less than 5 seconds for 1000 URIs)
assertThat(endTime - startTime).isLessThan(5000)
}
}
Loading