File: uri.go

package info (click to toggle)
golang-github-cue-lang-cue 0.12.0.-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 19,072 kB
  • sloc: sh: 57; makefile: 17
file content (225 lines) | stat: -rw-r--r-- 7,288 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package protocol

// This file declares URI, DocumentURI, and its methods.
//
// For the LSP definition of these types, see
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri

import (
	"fmt"
	"net/url"
	"path/filepath"
	"strings"
	"unicode"

	"cuelang.org/go/internal/golangorgx/gopls/util/pathutil"
)

// A DocumentURI is the URI of a client editor document.
//
// According to the LSP specification:
//
//	Care should be taken to handle encoding in URIs. For
//	example, some clients (such as VS Code) may encode colons
//	in drive letters while others do not. The URIs below are
//	both valid, but clients and servers should be consistent
//	with the form they use themselves to ensure the other party
//	doesn’t interpret them as distinct URIs. Clients and
//	servers should not assume that each other are encoding the
//	same way (for example a client encoding colons in drive
//	letters cannot assume server responses will have encoded
//	colons). The same applies to casing of drive letters - one
//	party should not assume the other party will return paths
//	with drive letters cased the same as it.
//
//	file:///c:/project/readme.md
//	file:///C%3A/project/readme.md
//
// This is done during JSON unmarshalling;
// see [DocumentURI.UnmarshalText] for details.
type DocumentURI string

// A URI is an arbitrary URL (e.g. https), not necessarily a file.
type URI = string

// UnmarshalText implements decoding of DocumentURI values.
//
// In particular, it implements a systematic correction of various odd
// features of the definition of DocumentURI in the LSP spec that
// appear to be workarounds for bugs in VS Code. For example, it may
// URI-encode the URI itself, so that colon becomes %3A, and it may
// send file://foo.go URIs that have two slashes (not three) and no
// hostname.
//
// We use UnmarshalText, not UnmarshalJSON, because it is called even
// for non-addressable values such as keys and values of map[K]V,
// where there is no pointer of type *K or *V on which to call
// UnmarshalJSON. (See Go issue #28189 for more detail.)
//
// Non-empty DocumentURIs are valid "file"-scheme URIs.
// The empty DocumentURI is valid.
func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
	*uri, err = ParseDocumentURI(string(data))
	return
}

// Path returns the file path for the given URI.
//
// DocumentURI("").Path() returns the empty string.
//
// Path panics if called on a URI that is not a valid filename.
func (uri DocumentURI) Path() string {
	filename, err := filename(uri)
	if err != nil {
		// e.g. ParseRequestURI failed.
		//
		// This can only affect DocumentURIs created by
		// direct string manipulation; all DocumentURIs
		// received from the client pass through
		// ParseRequestURI, which ensures validity.
		panic(err)
	}
	return filepath.FromSlash(filename)
}

// Dir returns the URI for the directory containing the receiver.
func (uri DocumentURI) Dir() DocumentURI {
	// filepath.Dir returns "." in case passed "". So we need to special
	// case a check for uri == "" in case the caller has not done that.
	if uri == "" {
		return ""
	}
	// This function could be more efficiently implemented by avoiding any call
	// to Path(), but at least consolidates URI manipulation.
	return URIFromPath(filepath.Dir(uri.Path()))
}

// Encloses reports whether uri's path, considered as a sequence of segments,
// is a prefix of file's path.
func (uri DocumentURI) Encloses(file DocumentURI) bool {
	return pathutil.InDir(uri.Path(), file.Path())
}

func filename(uri DocumentURI) (string, error) {
	if uri == "" {
		return "", nil
	}

	// This conservative check for the common case
	// of a simple non-empty absolute POSIX filename
	// avoids the allocation of a net.URL.
	if strings.HasPrefix(string(uri), "file:///") {
		rest := string(uri)[len("file://"):] // leave one slash
		for i := 0; i < len(rest); i++ {
			b := rest[i]
			// Reject these cases:
			if b < ' ' || b == 0x7f || // control character
				b == '%' || b == '+' || // URI escape
				b == ':' || // Windows drive letter
				b == '@' || b == '&' || b == '?' { // authority or query
				goto slow
			}
		}
		return rest, nil
	}
slow:

	u, err := url.ParseRequestURI(string(uri))
	if err != nil {
		return "", err
	}
	if u.Scheme != fileScheme {
		return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
	}
	// If the URI is a Windows URI, we trim the leading "/" and uppercase
	// the drive letter, which will never be case sensitive.
	if isWindowsDriveURIPath(u.Path) {
		u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
	}

	return u.Path, nil
}

// ParseDocumentURI interprets a string as a DocumentURI, applying VS
// Code workarounds; see [DocumentURI.UnmarshalText] for details.
func ParseDocumentURI(s string) (DocumentURI, error) {
	if s == "" {
		return "", nil
	}

	if !strings.HasPrefix(s, "file://") {
		return "", fmt.Errorf("DocumentURI scheme is not 'file': %s", s)
	}

	// VS Code sends URLs with only two slashes,
	// which are invalid. golang/go#39789.
	if !strings.HasPrefix(s, "file:///") {
		s = "file:///" + s[len("file://"):]
	}

	// Even though the input is a URI, it may not be in canonical form. VS Code
	// in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
	path, err := url.PathUnescape(s[len("file://"):])
	if err != nil {
		return "", err
	}

	// File URIs from Windows may have lowercase drive letters.
	// Since drive letters are guaranteed to be case insensitive,
	// we change them to uppercase to remain consistent.
	// For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
	if isWindowsDriveURIPath(path) {
		path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
	}
	u := url.URL{Scheme: fileScheme, Path: path}
	return DocumentURI(u.String()), nil
}

// URIFromPath returns DocumentURI for the supplied file path.
// Given "", it returns "".
func URIFromPath(path string) DocumentURI {
	if path == "" {
		return ""
	}
	if !isWindowsDrivePath(path) {
		if abs, err := filepath.Abs(path); err == nil {
			path = abs
		}
	}
	// Check the file path again, in case it became absolute.
	if isWindowsDrivePath(path) {
		path = "/" + strings.ToUpper(string(path[0])) + path[1:]
	}
	path = filepath.ToSlash(path)
	u := url.URL{
		Scheme: fileScheme,
		Path:   path,
	}
	return DocumentURI(u.String())
}

const fileScheme = "file"

// isWindowsDrivePath returns true if the file path is of the form used by
// Windows. We check if the path begins with a drive letter, followed by a ":".
// For example: C:/x/y/z.
func isWindowsDrivePath(path string) bool {
	if len(path) < 3 {
		return false
	}
	return unicode.IsLetter(rune(path[0])) && path[1] == ':'
}

// isWindowsDriveURIPath returns true if the file URI is of the format used by
// Windows URIs. The url.Parse package does not specially handle Windows paths
// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
func isWindowsDriveURIPath(uri string) bool {
	if len(uri) < 4 {
		return false
	}
	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
}