1 module served.lsp.uri;
2 
3 import served.lsp.protocol;
4 import std.algorithm;
5 import std.conv;
6 import std.path;
7 import std.string;
8 
9 DocumentUri uriFromFile(scope const(char)[] file)
10 {
11 	import std.uri : encodeComponent;
12 
13 	if (!isAbsolute(file))
14 		throw new Exception(text("Tried to pass relative path '", file, "' to uriFromFile"));
15 	file = file.buildNormalizedPath.replace("\\", "/");
16 	if (file.length == 0)
17 		return "";
18 	if (file[0] != '/')
19 		file = '/' ~ file; // always triple slash at start but never quad slash
20 	if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow
21 		file = file[2 .. $];
22 	return text("file://", file.encodeComponent.replace("%2F", "/"));
23 }
24 
25 string uriToFile(DocumentUri uri)
26 {
27 	import std.uri : decodeComponent;
28 	import std.string : startsWith;
29 
30 	if (uri.startsWith("file://"))
31 	{
32 		string ret = uri["file://".length .. $].decodeComponent;
33 		if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':')
34 			return ret[1 .. $].replace("/", "\\");
35 		else if (ret.length >= 1 && ret[0] != '/')
36 			return "\\\\" ~ ret.replace("/", "\\");
37 		return ret;
38 	}
39 	else
40 		return null;
41 }
42 
43 @system unittest
44 {
45 	void testUri(string a, string b)
46 	{
47 		void assertEqual(A, B)(A a, B b)
48 		{
49 			import std.conv : to;
50 
51 			assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string);
52 		}
53 
54 		assertEqual(a.uriFromFile, b);
55 		assertEqual(a, b.uriToFile);
56 		assertEqual(a.uriFromFile.uriToFile, a);
57 	}
58 
59 	version (Windows)
60 	{
61 		// taken from vscode-uri
62 		testUri(`c:\test with %\path`, `file:///c%3A/test%20with%20%25/path`);
63 		testUri(`c:\test with %25\path`, `file:///c%3A/test%20with%20%2525/path`);
64 		testUri(`c:\test with %25\c#code`, `file:///c%3A/test%20with%20%2525/c%23code`);
65 		testUri(`\\shäres\path\c#\plugin.json`, `file://sh%C3%A4res/path/c%23/plugin.json`);
66 		testUri(`\\localhost\c$\GitDevelopment\express`, `file://localhost/c%24/GitDevelopment/express`);
67 	}
68 	else version (Posix)
69 	{
70 		testUri(`/home/pi/.bashrc`, `file:///home/pi/.bashrc`);
71 		testUri(`/home/pi/Development Projects/D-code`, `file:///home/pi/Development%20Projects/D-code`);
72 	}
73 }
74 
75 DocumentUri uri(string scheme, string authority, string path, string query, string fragment)
76 {
77 	return scheme ~ "://"
78 		~ (authority.length ? authority : "")
79 		~ (path.length ? path : "/")
80 		~ (query.length ? "?" ~ query : "")
81 		~ (fragment.length ? "#" ~ fragment : "");
82 }
83 
84 DocumentUri uriDirName(DocumentUri uri)
85 {
86 	auto slash = uri.lastIndexOf('/');
87 	if (slash <= 0)
88 		return uri;
89 	if (uri[slash - 1] == '/')
90 	{
91 		if (slash == uri.length - 1)
92 			return uri;
93 		else
94 			return uri[0 .. slash + 1];
95 	}
96 	return uri[0 .. slash];
97 }
98 
99 ///
100 unittest
101 {
102 	assert("file:///foo/bar/".uriDirName == "file:///foo/bar");
103 	assert("file:///foo/bar".uriDirName == "file:///foo");
104 	assert("file:///foo/bar".uriDirName.uriDirName == "file:///");
105 }
106 
107 /// Appends the path to the uri, potentially replacing the whole thing if the
108 /// path is absolute. Cleans `./` and `../` sequences using `uriNormalize`.
109 DocumentUri uriBuildNormalized(DocumentUri uri, scope const(char)[] path)
110 {
111 	if (isAbsolute(path))
112 		return uriFromFile(path);
113 
114 	path = path.replace("\\", "/");
115 
116 	if (path.startsWith("/"))
117 	{
118 		auto scheme = uri.indexOf("://");
119 		if (scheme == -1)
120 			return path.idup;
121 		else
122 			return text(uri[0 .. scheme + 3], path[1 .. $]);
123 	}
124 
125 	while (path.startsWith("../"))
126 	{
127 		uri = uri.uriDirName;
128 		path = path[3 .. $];
129 	}
130 
131 	if (path.startsWith("/"))
132 		path = path[1 .. $];
133 
134 	return text(uri, "/", path).uriNormalize;
135 }
136 
137 ///
138 unittest
139 {
140 	assert(uriBuildNormalized("file:///foo/bar", "baz") == "file:///foo/bar/baz");
141 	assert(uriBuildNormalized("file:///foo/bar", "../baz") == "file:///foo/baz");
142 	version (Windows)
143 		assert(uriBuildNormalized("file:///foo/bar", `c:\home\baz`) == "file:///c%3A/home/baz");
144 	else
145 		assert(uriBuildNormalized("file:///foo/bar", "/home/baz") == "file:///home/baz");
146 }
147 
148 /// Cleans `./` and `../` from the URI
149 DocumentUri uriNormalize(DocumentUri uri)
150 {
151 	while (true)
152 	{
153 		auto sameDir = uri.indexOf("./");
154 		if (sameDir == -1)
155 			break;
156 		if (sameDir == 0)
157 			uri = uri[2 .. $];
158 		else if (sameDir >= 2 && uri[sameDir - 1] == '.' && uri[sameDir - 2] == '/') // /../
159 			uri = uri[0 .. sameDir - 1].uriDirName ~ uri[sameDir + 2 .. $];
160 		else if (uri[sameDir - 1] == '/') // /./
161 			uri = uri[0 .. sameDir] ~ uri[sameDir + 2 .. $];
162 		else
163 			break; // might break on `a./b` here, but better than infinite loop for malformed url
164 	}
165 
166 	return uri;
167 }