Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

122 lines
3.9 KiB
Go

// Package lspuri converts between filesystem paths and file:// URIs
// correctly on both POSIX and Windows.
//
// PURPOSE — the naive `"file://" + path` concatenation is correct on
// POSIX (an absolute path already starts with `/`, yielding the required
// three slashes) but BROKEN on Windows: `C:\repo\Main.java` becomes
// `file://C:\repo\Main.java` — the drive letter lands in the authority,
// backslashes are not URI separators, and the round-trip back to a path
// drops the drive. The net effect on Windows is a silent no-op: an LSP
// server (jdtls, etc.) cannot match the document we open, and the URIs it
// returns fail to map back to a repo path, so every definition/reference/
// implementation result is discarded and no edge is ever enriched.
//
// RATIONALE — centralise the conversion so didOpen, position requests,
// and result fold-back all agree on URI shape, and so the Windows-specific
// drive-letter / separator handling lives (and is tested) in exactly one
// place rather than being re-derived at each call site.
//
// KEYWORDS — file-uri, windows, drive-letter, lsp, path-conversion
package lspuri
import (
"net/url"
"path/filepath"
"strings"
)
// PathToURI converts a filesystem path to a file:// URI.
//
// POSIX: /repo/Main.java -> file:///repo/Main.java
// Windows: C:\repo\Main.java -> file:///C:/repo/Main.java
//
// Relative paths are made absolute first. Returns "" for an empty path.
func PathToURI(path string) string {
if path == "" {
return ""
}
abs, err := filepath.Abs(path)
if err != nil {
abs = path
}
return slashAbsToURI(filepath.ToSlash(abs))
}
// URIToAbsPath converts a file:// URI to an OS-native absolute path.
//
// file:///C:/repo/Main.java -> C:\repo\Main.java (Windows) / C:/repo/Main.java (POSIX runtime)
// file:///repo/Main.java -> /repo/Main.java
//
// Returns "" for non-file or unparseable URIs. URL-encoded octets
// (e.g. %20) are decoded.
func URIToAbsPath(uri string) string {
slash := uriToSlashAbs(uri)
if slash == "" {
return ""
}
return filepath.FromSlash(slash)
}
// URIToRepoRel converts a file:// URI to a forward-slash path relative to
// repoRoot, or "" when the URI is outside repoRoot or unparseable.
//
// Uses filepath.Rel (case-insensitive on Windows) rather than a string
// prefix so drive-letter casing and separator differences never cause a
// false miss — the failure mode that made LSP fold-back silently drop
// every result on Windows.
func URIToRepoRel(uri, repoRoot string) string {
abs := URIToAbsPath(uri)
if abs == "" || repoRoot == "" {
return ""
}
rel, err := filepath.Rel(repoRoot, abs)
if err != nil {
return ""
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return "" // outside the repo
}
return filepath.ToSlash(rel)
}
// slashAbsToURI builds a file URI from a forward-slash absolute path.
// Pure (no filepath / OS dependency) so it is deterministically testable
// for both POSIX ("/repo/x") and Windows-drive ("C:/repo/x") shapes.
func slashAbsToURI(slashAbs string) string {
if slashAbs == "" {
return ""
}
if !strings.HasPrefix(slashAbs, "/") {
slashAbs = "/" + slashAbs // C:/repo -> /C:/repo so the URI gets file:///C:/repo
}
return (&url.URL{Scheme: "file", Path: slashAbs}).String()
}
// uriToSlashAbs extracts the forward-slash absolute path from a file URI.
// Pure; testable for both shapes. Strips the spurious leading slash that
// precedes a Windows drive letter (/C:/.. -> C:/..).
func uriToSlashAbs(uri string) string {
if uri == "" {
return ""
}
parsed, err := url.Parse(uri)
if err != nil {
return ""
}
if parsed.Scheme != "" && parsed.Scheme != "file" {
return ""
}
p := parsed.Path
if p == "" {
p = parsed.Opaque // tolerate `file:relative` shapes
}
if len(p) >= 3 && p[0] == '/' && isDriveLetter(p[1]) && p[2] == ':' {
p = p[1:] // /C:/repo -> C:/repo
}
return p
}
func isDriveLetter(c byte) bool {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}